Merge "Replace BuildCompat.isAndroidU with the proper check." into androidx-main
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
index aa6bea5..a50e8a7 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
@@ -25,13 +25,13 @@
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
 import androidx.benchmark.userspaceTrace
 import androidx.test.platform.app.InstrumentationRegistry
-import androidx.tracing.perfetto.PerfettoSdkHandshake
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ERROR_OTHER
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_SUCCESS
+import androidx.tracing.perfetto.handshake.PerfettoSdkHandshake
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ERROR_OTHER
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_SUCCESS
 import java.io.File
 import java.io.StringReader
 
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
index ec10dc1..42e0144 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
@@ -28,12 +28,12 @@
 import androidx.benchmark.perfetto.PerfettoCapture
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
 import androidx.test.platform.app.InstrumentationRegistry
-import androidx.tracing.perfetto.PerfettoSdkHandshake
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_CANCELLED
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ERROR_OTHER
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_SUCCESS
+import androidx.tracing.perfetto.handshake.PerfettoSdkHandshake
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_CANCELLED
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ERROR_OTHER
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_SUCCESS
 import com.google.common.truth.Truth.assertThat
 import java.io.File
 import java.io.StringReader
@@ -161,11 +161,11 @@
     }
 
     /**
-     * This tests [androidx.tracing.perfetto.PerfettoSdkHandshake] which is used by both Benchmark
-     * and Studio.
+     * This tests [androidx.tracing.perfetto.handshake.PerfettoSdkHandshake] which is used by both
+     * Benchmark and Studio.
      *
      * By contrast, other tests use the [PerfettoCapture.enableAndroidxTracingPerfetto], which
-     * is built on top of [androidx.tracing.perfetto.PerfettoSdkHandshake] and implements
+     * is built on top of [androidx.tracing.perfetto.handshake.PerfettoSdkHandshake] and implements
      * the parts where Studio and Benchmark differ.
      */
     @Test
@@ -173,7 +173,7 @@
         assumeTrue(isAbiSupported())
         assumeTrue(Build.VERSION.SDK_INT >= minSupportedSdk)
 
-        /** perform a handshake using [androidx.tracing.perfetto.PerfettoSdkHandshake] */
+        /** perform a handshake using [androidx.tracing.perfetto.handshake.PerfettoSdkHandshake] */
         val libraryZip: File? = resolvePerfettoAar()
         val tmpDir = Outputs.dirUsableByAppAndShell
         val mvTmpDst = createShellFileMover()
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
index 34c2fa3..b6ff946 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
@@ -96,8 +96,9 @@
     @Test
     fun shouldFail_whenCaptureConfigSurfaceNotRecognized() {
         // Arrange
+        val fakeSurface = FakeSurface()
         val captureConfig = CaptureConfig.Builder()
-            .apply { addSurface(FakeSurface()) }
+            .apply { addSurface(fakeSurface) }
             .build()
         val sessionConfigOptions = Camera2ImplConfig.Builder().build()
 
@@ -109,6 +110,9 @@
                 sessionConfigOptions
             )
         }
+
+        // Clean up
+        fakeSurface.close()
     }
 
     @Test
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
index d4c2e51..8896087 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
@@ -178,6 +178,10 @@
     fun tearDown() {
         // CoroutineScope#cancel can throw exception if the scope has no job left
         try {
+            fakeUseCaseCamera.runningUseCases.forEach {
+                it.onStateDetached()
+                it.onUnbind()
+            }
             // fakeUseCaseThreads may still be using Main dispatcher which sometimes
             // causes Dispatchers.resetMain() to throw an exception:
             // "IllegalStateException: Dispatchers.Main is used concurrently with setting it"
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/PreviewPixelHDRnetQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/PreviewPixelHDRnetQuirkTest.kt
index 08f756f..5542e0b 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/PreviewPixelHDRnetQuirkTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/PreviewPixelHDRnetQuirkTest.kt
@@ -103,7 +103,7 @@
     @Test
     fun previewShouldApplyToneModeForHDRNet() {
         // Arrange
-        val cameraUseCaseAdapter = configureCameraUseCaseAdapter(
+        cameraUseCaseAdapter = configureCameraUseCaseAdapter(
             resolutionVGA,
             configType = PreviewConfig::class.java
         )
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
index baa17cc..945507d 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
@@ -49,6 +49,7 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runTest
+import org.junit.After
 import org.junit.Assert.assertThrows
 import org.junit.Assume.assumeTrue
 import org.junit.Before
@@ -109,6 +110,11 @@
         stillCaptureRequestControl.setNewUseCaseCamera()
     }
 
+    @After
+    fun tearDown() {
+        fakeSurface.close()
+    }
+
     @Test
     fun captureRequestsSubmitted_whenCameraIsSet() = runTest(testDispatcher) {
         stillCaptureRequestControl.issueCaptureRequests()
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
index c129403..00f0f4c 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
@@ -49,6 +49,7 @@
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.runBlocking
+import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
@@ -93,6 +94,11 @@
         useCaseGraphConfig = fakeUseCaseGraphConfig,
     )
 
+    @After
+    fun tearDown() {
+        surface.close()
+    }
+
     @Test
     fun testMergeRequestOptions(): Unit = runBlocking {
         // Arrange
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraStateTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraStateTest.kt
index a724e46..abd71a4 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraStateTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraStateTest.kt
@@ -42,6 +42,7 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runTest
+import org.junit.After
 import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Rule
@@ -89,6 +90,11 @@
         fakeCameraGraphSession.startRepeatingSignal = CompletableDeferred() // not complete yet
     }
 
+    @After
+    fun tearDown() {
+        surface.close()
+    }
+
     @Test
     fun updateAsyncCompletes_whenStopRepeating(): Unit = runBlocking {
         // stopRepeating is called when there is no stream after updateAsync call
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
index 20d762e..c1d3ac6 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
@@ -41,6 +41,7 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.asExecutor
+import org.junit.After
 import org.junit.Assume.assumeTrue
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -89,6 +90,11 @@
         useCaseGraphConfig = fakeUseCaseGraphConfig,
     )
 
+    @After
+    fun tearDown() {
+        surface.close()
+    }
+
     @Test
     fun setInvalidSessionConfig_repeatingShouldStop() {
         // Arrange
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
index 605e1fb..9d76b98 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.windowInsetsPadding
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.geometry.Offset
@@ -43,6 +44,7 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.toSize
@@ -271,7 +273,7 @@
     @Test
     fun scaffold_providesInsets_respectTopAppBar() {
         rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 20.dp)) {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
                 Scaffold(
                     contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
                     topBar = {
@@ -279,11 +281,80 @@
                     }
                 ) { paddingValues ->
                     // top is like top app bar + rounding error
-                    assertThat(paddingValues.calculateTopPadding() - 10.dp)
-                        .isLessThan(roundingError)
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 10.dp,
+                        threshold = roundingError
+                    )
                     // bottom is like the insets
-                    assertThat(paddingValues.calculateBottomPadding() - 30.dp).isLessThan(
-                        roundingError
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 3.dp,
+                        threshold = roundingError
+                    )
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_respectsProvidedInsets() {
+        rule.setContent {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 15.dp, bottom = 10.dp),
+                ) { paddingValues ->
+                    // topPadding is equal to provided top window inset
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 15.dp,
+                        threshold = roundingError
+                    )
+                    // bottomPadding is equal to provided bottom window inset
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 10.dp,
+                        threshold = roundingError
+                    )
+                    Box(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(color = Color.White)
+                    )
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun scaffold_respectsConsumedWindowInsets() {
+        rule.setContent {
+            Box(
+                Modifier
+                    .requiredSize(10.dp, 40.dp)
+                    .windowInsetsPadding(WindowInsets(top = 10.dp, bottom = 10.dp))
+            ) {
+                Scaffold(
+                    contentWindowInsets = WindowInsets(top = 15.dp, bottom = 15.dp)
+                ) { paddingValues ->
+                    // Consumed windowInsetsPadding is omitted. This replicates behavior from
+                    // Modifier.windowInsetsPadding. (15.dp contentPadding - 10.dp consumedPadding)
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 5.dp,
+                        threshold = roundingError
+                    )
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 5.dp,
+                        threshold = roundingError
                     )
                     Box(
                         Modifier
@@ -299,7 +370,7 @@
     @Test
     fun scaffold_providesInsets_respectCollapsedTopAppBar() {
         rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 20.dp)) {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
                 Scaffold(
                     contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
                     topBar = {
@@ -307,10 +378,16 @@
                     }
                 ) { paddingValues ->
                     // top is like the collapsed top app bar (i.e. 0dp) + rounding error
-                    assertThat(paddingValues.calculateTopPadding()).isLessThan(roundingError)
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 0.dp,
+                        threshold = roundingError
+                    )
                     // bottom is like the insets
-                    assertThat(paddingValues.calculateBottomPadding() - 30.dp).isLessThan(
-                        roundingError
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 3.dp,
+                        threshold = roundingError
                     )
                     Box(
                         Modifier
@@ -326,7 +403,7 @@
     @Test
     fun scaffold_providesInsets_respectsBottomAppBar() {
         rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 20.dp)) {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
                 Scaffold(
                     contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
                     bottomBar = {
@@ -334,11 +411,17 @@
                     }
                 ) { paddingValues ->
                     // bottom is like bottom app bar + rounding error
-                    assertThat(paddingValues.calculateBottomPadding() - 10.dp).isLessThan(
-                        roundingError
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateBottomPadding(),
+                        expected = 10.dp,
+                        threshold = roundingError
                     )
                     // top is like the insets
-                    assertThat(paddingValues.calculateTopPadding() - 5.dp).isLessThan(roundingError)
+                    assertDpIsWithinThreshold(
+                        actual = paddingValues.calculateTopPadding(),
+                        expected = 5.dp,
+                        threshold = roundingError
+                    )
                     Box(
                         Modifier
                             .requiredSize(10.dp)
@@ -357,7 +440,7 @@
         var snackbarPosition: Offset? = null
         var density: Density? = null
         rule.setContent {
-            Box(Modifier.requiredSize(10.dp, 20.dp)) {
+            Box(Modifier.requiredSize(10.dp, 40.dp)) {
                 density = LocalDensity.current
                 Scaffold(
                     contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp),
@@ -417,4 +500,8 @@
             with(density!!) { (fabPosition!!.y.roundToInt() + fabSize!!.height).toDp() }
         assertThat(rule.rootHeight() - fabBottomOffsetDp - 3.dp).isLessThan(1.dp)
     }
+
+    private fun assertDpIsWithinThreshold(actual: Dp, expected: Dp, threshold: Dp) {
+        assertThat(actual.value).isWithin(threshold.value).of(expected.value)
+    }
 }
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.kt
index 8b84f83..92c0bae 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.kt
@@ -65,9 +65,7 @@
 import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.runtime.structuralEqualityPolicy
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
@@ -93,7 +91,6 @@
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.VisualTransformation
 import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
@@ -695,30 +692,6 @@
     override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = 0.dp
 }
 
-/**
- * A [WindowInsets] whose values can change without changing the instance. This is useful
- * to avoid recomposition when [WindowInsets] can change.
- *
- * Copied from [androidx.compose.foundation.layout.MutableWindowInsets], which is marked as
- * experimental and thus cannot be used cross-module.
- */
-private class MutableWindowInsets(
-    initialInsets: WindowInsets = WindowInsets(0, 0, 0, 0)
-) : WindowInsets {
-    /**
-     * The [WindowInsets] that are used for [left][getLeft], [top][getTop], [right][getRight],
-     * and [bottom][getBottom] values.
-     */
-    var insets by mutableStateOf(initialInsets)
-
-    override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int =
-        insets.getLeft(density, layoutDirection)
-    override fun getTop(density: Density): Int = insets.getTop(density)
-    override fun getRight(density: Density, layoutDirection: LayoutDirection): Int =
-        insets.getRight(density, layoutDirection)
-    override fun getBottom(density: Density): Int = insets.getBottom(density)
-}
-
 // Measurement specs
 @OptIn(ExperimentalMaterial3Api::class)
 private val SearchBarCornerRadius: Dp = InputFieldHeight / 2
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MutableWindowInsets.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MutableWindowInsets.kt
new file mode 100644
index 0000000..95ac756d
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MutableWindowInsets.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * A [WindowInsets] whose values can change without changing the instance. This is useful
+ * to avoid recomposition when [WindowInsets] can change.
+ *
+ * Copied from [androidx.compose.foundation.layout.MutableWindowInsets], which is marked as
+ * experimental and thus cannot be used cross-module.
+ */
+internal class MutableWindowInsets(
+    initialInsets: WindowInsets = WindowInsets(0, 0, 0, 0)
+) : WindowInsets {
+    /**
+     * The [WindowInsets] that are used for [left][getLeft], [top][getTop], [right][getRight],
+     * and [bottom][getBottom] values.
+     */
+    var insets by mutableStateOf(initialInsets)
+
+    override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int =
+        insets.getLeft(density, layoutDirection)
+    override fun getTop(density: Density): Int = insets.getTop(density)
+    override fun getRight(density: Density, layoutDirection: LayoutDirection): Int =
+        insets.getRight(density, layoutDirection)
+    override fun getBottom(density: Density): Int = insets.getBottom(density)
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
index 122937e..1946195 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
@@ -21,9 +21,13 @@
 import androidx.compose.foundation.layout.asPaddingValues
 import androidx.compose.foundation.layout.calculateEndPadding
 import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.exclude
+import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -64,7 +68,8 @@
  * @param contentWindowInsets window insets to be passed to [content] slot via [PaddingValues]
  * params. Scaffold will take the insets into account from the top/bottom only if the [topBar]/
  * [bottomBar] are not present, as the scaffold expect [topBar]/[bottomBar] to handle insets
- * instead
+ * instead. Any insets consumed by other insets padding modifiers or [consumeWindowInsets] on a
+ * parent layout will be excluded from [contentWindowInsets].
  * @param content content of the screen. The lambda receives a [PaddingValues] that should be
  * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
  * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
@@ -83,14 +88,23 @@
     contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
     content: @Composable (PaddingValues) -> Unit
 ) {
-    Surface(modifier = modifier, color = containerColor, contentColor = contentColor) {
+    val safeInsets = remember(contentWindowInsets) {
+        MutableWindowInsets(contentWindowInsets)
+    }
+    Surface(
+        modifier = modifier.onConsumedWindowInsetsChanged { consumedWindowInsets ->
+            // Exclude currently consumed window insets from user provided contentWindowInsets
+            safeInsets.insets = contentWindowInsets.exclude(consumedWindowInsets)
+        },
+        color = containerColor,
+        contentColor = contentColor) {
         ScaffoldLayout(
             fabPosition = floatingActionButtonPosition,
             topBar = topBar,
             bottomBar = bottomBar,
             content = content,
             snackbar = snackbarHost,
-            contentWindowInsets = contentWindowInsets,
+            contentWindowInsets = safeInsets,
             fab = floatingActionButton
         )
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 7d9003a3..4b840ca 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -377,14 +377,14 @@
 
     private var currentIndex = 0
     private var currentPostLookaheadIndex = 0
-    private val nodeToNodeState = mutableMapOf<LayoutNode, NodeState>()
+    private val nodeToNodeState = hashMapOf<LayoutNode, NodeState>()
 
     // this map contains active slotIds (without precomposed or reusable nodes)
-    private val slotIdToNode = mutableMapOf<Any?, LayoutNode>()
+    private val slotIdToNode = hashMapOf<Any?, LayoutNode>()
     private val scope = Scope()
     private val postLookaheadMeasureScope = PostLookaheadMeasureScopeImpl()
 
-    private val precomposeMap = mutableMapOf<Any?, LayoutNode>()
+    private val precomposeMap = hashMapOf<Any?, LayoutNode>()
     private val reusableSlotIdsSet = SubcomposeSlotReusePolicy.SlotIdsSet()
     // SlotHandles precomposed in the post-lookahead pass.
     private val postLookaheadPrecomposeSlotHandleMap = mutableMapOf<Any?, PrecomposedSlotHandle>()
@@ -427,13 +427,16 @@
             }
         }
 
-        val itemIndex = root.foldedChildren.indexOf(node)
-        require(itemIndex >= currentIndex) {
-            "Key \"$slotId\" was already used. If you are using LazyColumn/Row please make " +
-                "sure you provide a unique key for each item."
-        }
-        if (currentIndex != itemIndex) {
-            move(itemIndex, currentIndex)
+        if (root.foldedChildren.getOrNull(currentIndex) !== node) {
+            // the node has a new index in the list
+            val itemIndex = root.foldedChildren.indexOf(node)
+            require(itemIndex >= currentIndex) {
+                "Key \"$slotId\" was already used. If you are using LazyColumn/Row please make " +
+                    "sure you provide a unique key for each item."
+            }
+            if (currentIndex != itemIndex) {
+                move(itemIndex, currentIndex)
+            }
         }
         currentIndex++
 
@@ -548,14 +551,15 @@
     }
 
     fun makeSureStateIsConsistent() {
-        require(nodeToNodeState.size == root.foldedChildren.size) {
+        val childrenCount = root.foldedChildren.size
+        require(nodeToNodeState.size == childrenCount) {
             "Inconsistency between the count of nodes tracked by the state " +
                 "(${nodeToNodeState.size}) and the children count on the SubcomposeLayout" +
-                " (${root.foldedChildren.size}). Are you trying to use the state of the" +
+                " ($childrenCount). Are you trying to use the state of the" +
                 " disposed SubcomposeLayout?"
         }
-        require(root.foldedChildren.size - reusableCount - precomposedCount >= 0) {
-            "Incorrect state. Total children ${root.foldedChildren.size}. Reusable children " +
+        require(childrenCount - reusableCount - precomposedCount >= 0) {
+            "Incorrect state. Total children $childrenCount. Reusable children " +
                 "$reusableCount. Precomposed children $precomposedCount"
         }
         require(precomposeMap.size == precomposedCount) {
diff --git a/libraryversions.toml b/libraryversions.toml
index 637f4fc..edcbaae 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -134,7 +134,7 @@
 TRACING = "1.3.0-alpha01"
 TRACING_PERFETTO = "1.0.0-alpha16"
 TRANSITION = "1.5.0-alpha01"
-TV = "1.0.0-alpha07"
+TV = "1.0.0-alpha08"
 TVPROVIDER = "1.1.0-alpha02"
 VECTORDRAWABLE = "1.2.0-rc01"
 VECTORDRAWABLE_ANIMATED = "1.2.0-rc01"
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SandboxedSdkContextCompatTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SandboxedSdkContextCompatTest.kt
index 11c62973..42f7b49e 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SandboxedSdkContextCompatTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SandboxedSdkContextCompatTest.kt
@@ -15,27 +15,387 @@
  */
 package androidx.privacysandbox.sdkruntime.client.loader
 
+import android.content.Context
+import android.os.Build
 import androidx.privacysandbox.sdkruntime.client.loader.impl.SandboxedSdkContextCompat
 import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
+import java.io.DataInputStream
+import java.io.DataOutputStream
+import java.io.File
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 
 @SmallTest
-@RunWith(AndroidJUnit4::class)
-class SandboxedSdkContextCompatTest {
+@RunWith(Parameterized::class)
+internal class SandboxedSdkContextCompatTest(
+    private val contextType: String,
+    private val sdkContextCompat: SandboxedSdkContextCompat,
+    private val appStorageContext: Context
+) {
 
     @Test
     fun getClassloader_returnSdkClassloader() {
         val sdkClassLoader = javaClass.classLoader!!.parent!!
-
-        val sdkContextCompat = SandboxedSdkContextCompat(
-            ApplicationProvider.getApplicationContext(),
-            sdkClassLoader
-        )
         assertThat(sdkContextCompat.classLoader)
             .isEqualTo(sdkClassLoader)
     }
+
+    @Test
+    fun getDataDir_returnSdkDataDirInAppDir() {
+        val expectedSdksRoot = appStorageContext.getDir(SDK_ROOT_FOLDER, Context.MODE_PRIVATE)
+        val expectedSdkDataDir = File(expectedSdksRoot, SDK_PACKAGE_NAME)
+
+        assertThat(sdkContextCompat.dataDir)
+            .isEqualTo(expectedSdkDataDir)
+
+        assertThat(expectedSdkDataDir.exists()).isTrue()
+    }
+
+    @Test
+    fun getCacheDir_returnSdkCacheDirInAppCacheDir() {
+        val expectedSdksCacheRoot = File(appStorageContext.cacheDir, SDK_ROOT_FOLDER)
+        val expectedSdkCache = File(expectedSdksCacheRoot, SDK_PACKAGE_NAME)
+
+        assertThat(sdkContextCompat.cacheDir)
+            .isEqualTo(expectedSdkCache)
+
+        assertThat(expectedSdkCache.exists()).isTrue()
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 21)
+    fun getCodeCacheDir_returnSdkCodeCacheDirInAppCodeCacheDir() {
+        val expectedSdksCodeCacheRoot = File(appStorageContext.codeCacheDir, SDK_ROOT_FOLDER)
+        val expectedSdkCodeCache = File(expectedSdksCodeCacheRoot, SDK_PACKAGE_NAME)
+
+        assertThat(sdkContextCompat.codeCacheDir)
+            .isEqualTo(expectedSdkCodeCache)
+
+        assertThat(expectedSdkCodeCache.exists()).isTrue()
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 21)
+    fun getNoBackupFilesDir_returnSdkNoBackupDirInAppNoBackupDir() {
+        val expectedSdksNoBackupRoot = File(appStorageContext.noBackupFilesDir, SDK_ROOT_FOLDER)
+        val expectedSdkNoBackupDir = File(expectedSdksNoBackupRoot, SDK_PACKAGE_NAME)
+
+        assertThat(sdkContextCompat.noBackupFilesDir)
+            .isEqualTo(expectedSdkNoBackupDir)
+
+        assertThat(expectedSdkNoBackupDir.exists()).isTrue()
+    }
+
+    @Test
+    fun getDir_returnDirWithPrefixInSdkDataDir() {
+        val expectedDir = File(sdkContextCompat.dataDir, "app_test")
+
+        assertThat(sdkContextCompat.getDir("test", Context.MODE_PRIVATE))
+            .isEqualTo(expectedDir)
+
+        assertThat(expectedDir.exists()).isTrue()
+    }
+
+    @Test
+    fun getFilesDir_returnFilesDirInSdkDataDir() {
+        val expectedFilesDir = File(sdkContextCompat.dataDir, "files")
+
+        assertThat(sdkContextCompat.filesDir)
+            .isEqualTo(expectedFilesDir)
+
+        assertThat(expectedFilesDir.exists()).isTrue()
+    }
+
+    @Test
+    fun openFileInput_openFileInSdkFilesDir() {
+        val fileToOpen = File(sdkContextCompat.filesDir, "testOpenFileInput")
+        fileToOpen.outputStream().use { outputStream ->
+            DataOutputStream(outputStream).use { dataStream ->
+                dataStream.writeInt(42)
+            }
+        }
+
+        val content = sdkContextCompat.openFileInput("testOpenFileInput")
+            .use { inputStream ->
+                DataInputStream(inputStream).use { dataStream ->
+                    dataStream.readInt()
+                }
+            }
+
+        assertThat(content)
+            .isEqualTo(42)
+    }
+
+    @Test
+    fun openFileInput_whenFileNameContainsFileSeparator_throwsIllegalArgumentException() {
+        assertThrows<IllegalArgumentException> {
+            sdkContextCompat.openFileInput("folder/file")
+        }
+    }
+
+    @Test
+    fun openFileOutput_openFileInSdkFilesDir() {
+        sdkContextCompat.openFileOutput("testOpenFileOutput", Context.MODE_PRIVATE)
+            .use { outputStream ->
+                DataOutputStream(outputStream).use { dataStream ->
+                    dataStream.writeInt(42)
+                }
+            }
+
+        val expectedFile = File(sdkContextCompat.filesDir, "testOpenFileOutput")
+        val content = expectedFile.inputStream().use { inputStream ->
+            DataInputStream(inputStream).use { dataStream ->
+                dataStream.readInt()
+            }
+        }
+
+        assertThat(content)
+            .isEqualTo(42)
+    }
+
+    @Test
+    fun openFileOutput_whenAppendFlagSet_appendToFileInSdkFilesDir() {
+        sdkContextCompat.openFileOutput(
+            "testOpenFileOutputAppend",
+            Context.MODE_PRIVATE or Context.MODE_APPEND
+        ).use { outputStream ->
+            DataOutputStream(outputStream).use { dataStream ->
+                dataStream.writeInt(42)
+            }
+        }
+        sdkContextCompat.openFileOutput(
+            "testOpenFileOutputAppend",
+            Context.MODE_PRIVATE or Context.MODE_APPEND
+        ).use { outputStream ->
+            DataOutputStream(outputStream).use { dataStream ->
+                dataStream.writeInt(1)
+            }
+        }
+
+        val expectedFile = File(sdkContextCompat.filesDir, "testOpenFileOutputAppend")
+        val content = expectedFile.inputStream().use { inputStream ->
+            DataInputStream(inputStream).use { dataStream ->
+                dataStream.readInt() + dataStream.readInt()
+            }
+        }
+
+        assertThat(content)
+            .isEqualTo(43)
+    }
+
+    @Test
+    fun openFileOutput_whenFileNameContainsFileSeparator_throwsIllegalArgumentException() {
+        assertThrows<IllegalArgumentException> {
+            sdkContextCompat.openFileOutput("folder/file", Context.MODE_PRIVATE)
+        }
+    }
+
+    @Test
+    fun deleteFile_deleteFileInSdkFilesDir() {
+        val fileToDelete = File(sdkContextCompat.filesDir, "testDelete")
+        fileToDelete.createNewFile()
+        assertThat(fileToDelete.exists()).isTrue()
+
+        assertThat(sdkContextCompat.deleteFile("testDelete")).isTrue()
+        assertThat(fileToDelete.exists()).isFalse()
+    }
+
+    @Test
+    fun deleteFile_whenFileNameContainsFileSeparator_throwsIllegalArgumentException() {
+        assertThrows<IllegalArgumentException> {
+            sdkContextCompat.deleteFile("folder/file")
+        }
+    }
+
+    @Test
+    fun getFileStreamPath_returnFileFromSdkFilesDir() {
+        val expectedFile = File(sdkContextCompat.filesDir, "testGetFileStreamPath")
+
+        assertThat(sdkContextCompat.getFileStreamPath("testGetFileStreamPath"))
+            .isEqualTo(expectedFile)
+    }
+
+    @Test
+    fun getFileStreamPath_whenFileNameContainsFileSeparator_throwsIllegalArgumentException() {
+        assertThrows<IllegalArgumentException> {
+            sdkContextCompat.getFileStreamPath("folder/file")
+        }
+    }
+
+    @Test
+    fun fileList_returnContentOfSdkFilesDir() {
+        val expectedFile = File(sdkContextCompat.filesDir, "testFileList")
+        expectedFile.createNewFile()
+
+        val result = sdkContextCompat.fileList().asList()
+        assertThat(result).contains("testFileList")
+        assertThat(result).isEqualTo(sdkContextCompat.filesDir.list()!!.asList())
+    }
+
+    @Test
+    fun getDatabasePath_whenDataBaseNamePassed_returnPathToDatabaseInSdkDatabasesDir() {
+        val expectedDatabasePath = File(
+            sdkContextCompat.dataDir,
+            "databases/testGetDatabasePath"
+        )
+
+        assertThat(sdkContextCompat.getDatabasePath("testGetDatabasePath"))
+            .isEqualTo(expectedDatabasePath)
+    }
+
+    @Test
+    fun getDatabasePath_whenDataBasePathPassed_returnSamePath() {
+        val expectedDatabasePath = File(
+            sdkContextCompat.dataDir,
+            "databases/testGetDatabasePathAbsolute"
+        )
+
+        assertThat(sdkContextCompat.getDatabasePath(expectedDatabasePath.absolutePath))
+            .isEqualTo(expectedDatabasePath)
+    }
+
+    @Test
+    fun openOrCreateDatabase_returnDatabaseFromSdkDatabasesDir() {
+        val databaseName = "testOpenDataBase.db"
+
+        sdkContextCompat.deleteDatabase(databaseName)
+        val database = sdkContextCompat.openOrCreateDatabase(
+            name = databaseName,
+            mode = Context.MODE_PRIVATE,
+            factory = null
+        )
+
+        database.execSQL("CREATE TABLE test (data int)")
+        database.execSQL("INSERT INTO test (data) values (42)")
+
+        val databaseFrom4ParamMethod = sdkContextCompat.openOrCreateDatabase(
+            name = databaseName,
+            mode = Context.MODE_PRIVATE,
+            factory = null,
+            errorHandler = null
+        )
+
+        val result = databaseFrom4ParamMethod.rawQuery("SELECT * FROM test", null)
+        result.moveToFirst()
+        assertThat(result.getInt(0))
+            .isEqualTo(42)
+
+        val databasePath = sdkContextCompat.getDatabasePath(databaseName)
+        assertThat(databasePath.exists()).isTrue()
+    }
+
+    @Test
+    fun deleteDatabase_deleteDatabaseFromSdkDatabasesDir() {
+        val databaseName = "testDeleteDatabase.db"
+        sdkContextCompat.openOrCreateDatabase(
+            name = databaseName,
+            mode = Context.MODE_PRIVATE,
+            factory = null
+        )
+        assertThat(sdkContextCompat.getDatabasePath(databaseName).exists()).isTrue()
+
+        sdkContextCompat.deleteDatabase(databaseName)
+
+        assertThat(sdkContextCompat.getDatabasePath(databaseName).exists()).isFalse()
+    }
+
+    @Test
+    fun databaseList_returnContentOfSdkDatabasesDir() {
+        val databaseName = "testDatabaseList.db"
+        sdkContextCompat.openOrCreateDatabase(
+            name = databaseName,
+            mode = Context.MODE_PRIVATE,
+            factory = null
+        )
+
+        val result = sdkContextCompat.databaseList().asList()
+        assertThat(result).contains(databaseName)
+        assertThat(result).isEqualTo(
+            File(sdkContextCompat.dataDir, "databases").list()!!.asList()
+        )
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    fun moveDatabaseFrom_migrateDatabaseToSdkDatabasesDir() {
+        val sourceAppStorageContext = if (sdkContextCompat.isDeviceProtectedStorage) {
+            ApplicationProvider.getApplicationContext()
+        } else {
+            appStorageContext.createDeviceProtectedStorageContext()
+        }
+        val sourceContext = SandboxedSdkContextCompat(
+            sourceAppStorageContext,
+            sdkPackageName = SDK_PACKAGE_NAME,
+            classLoader = javaClass.classLoader!!.parent!!
+        )
+
+        val databaseName = "testMoveTo$contextType.db"
+
+        sourceContext.deleteDatabase(databaseName)
+        val database = sourceContext.openOrCreateDatabase(
+            name = databaseName,
+            mode = Context.MODE_PRIVATE,
+            factory = null
+        )
+
+        database.execSQL("CREATE TABLE test (data int)")
+        database.execSQL("INSERT INTO test (data) values (42)")
+
+        val moveResult = sdkContextCompat.moveDatabaseFrom(sourceContext, databaseName)
+        assertThat(moveResult).isTrue()
+
+        val migratedDatabase = sdkContextCompat.openOrCreateDatabase(
+            name = databaseName,
+            mode = Context.MODE_PRIVATE,
+            factory = null
+        )
+
+        val result = migratedDatabase.rawQuery("SELECT * FROM test", null)
+        result.moveToFirst()
+        assertThat(result.getInt(0))
+            .isEqualTo(42)
+
+        val oldDatabasePath = sourceContext.getDatabasePath(databaseName)
+        assertThat(oldDatabasePath.exists()).isFalse()
+    }
+
+    companion object {
+        private const val SDK_ROOT_FOLDER = "RuntimeEnabledSdksData"
+        private const val SDK_PACKAGE_NAME = "androidx.privacysandbox.sdkruntime.testsdk1"
+
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun params(): List<Array<Any>> = buildList {
+            val appContext = ApplicationProvider.getApplicationContext<Context>()
+
+            val sdkContext = SandboxedSdkContextCompat(
+                appContext,
+                sdkPackageName = SDK_PACKAGE_NAME,
+                classLoader = javaClass.classLoader!!.parent!!
+            )
+            add(
+                arrayOf(
+                    "SimpleContext",
+                    sdkContext,
+                    appContext
+                )
+            )
+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                val deviceProtectedSdkContext = sdkContext.createDeviceProtectedStorageContext()
+                add(
+                    arrayOf(
+                        "DeviceProtectedStorageContext",
+                        deviceProtectedSdkContext,
+                        appContext.createDeviceProtectedStorageContext()
+                    )
+                )
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
index 57db8c7..dceeeff1 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
@@ -90,6 +90,21 @@
     }
 
     @Test
+    fun testContextFilesDir() {
+        val loadedSdk = sdkLoader.loadSdk(testSdkConfig)
+
+        val sdkContext = loadedSdk.extractSdkContext()
+
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val expectedSdksRoot = context.getDir("RuntimeEnabledSdksData", Context.MODE_PRIVATE)
+        val expectedSdkData = File(expectedSdksRoot, testSdkConfig.packageName)
+        val expectedSdkFilesDir = File(expectedSdkData, "files")
+
+        assertThat(sdkContext.filesDir)
+            .isEqualTo(expectedSdkFilesDir)
+    }
+
+    @Test
     fun testJavaResources() {
         val loadedSdk = sdkLoader.loadSdk(testSdkConfig)
 
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/impl/MigrationUtilsTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/impl/MigrationUtilsTest.kt
new file mode 100644
index 0000000..2972d47
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/impl/MigrationUtilsTest.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.sdkruntime.client.loader.impl
+
+import android.content.Context
+import android.os.Build
+import android.system.Os
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import java.io.DataInputStream
+import java.io.DataOutputStream
+import java.io.File
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+class MigrationUtilsTest {
+
+    private lateinit var context: Context
+    private lateinit var fromDir: File
+    private lateinit var toDir: File
+
+    @Before
+    fun setUp() {
+        context = ApplicationProvider.getApplicationContext()
+
+        // Clean up between tests
+        val testDir = File(context.cacheDir, "MigrationUtilsTest")
+        testDir.deleteRecursively()
+        testDir.deleteOnExit()
+
+        fromDir = File(testDir, "from")
+        fromDir.mkdirs()
+
+        toDir = File(testDir, "to")
+        toDir.mkdirs()
+    }
+
+    @Test
+    fun moveFiles_moveFileContents() {
+        val fileToMove = File(fromDir, "testFile")
+        fileToMove.createNewFile()
+        fileToMove.outputStream().use { outputStream ->
+            DataOutputStream(outputStream).use { dataStream ->
+                dataStream.writeInt(42)
+            }
+        }
+
+        val result = MigrationUtils.moveFiles(fromDir, toDir, fileToMove.name)
+        assertThat(result).isTrue()
+        assertThat(fileToMove.exists()).isFalse()
+
+        val resultFile = File(toDir, fileToMove.name)
+        assertThat(resultFile.exists()).isTrue()
+
+        val content = resultFile.inputStream().use { inputStream ->
+            DataInputStream(inputStream).use { dataStream ->
+                dataStream.readInt()
+            }
+        }
+
+        assertThat(content)
+            .isEqualTo(42)
+    }
+
+    @Test
+    fun moveFiles_copyPermissions() {
+        val fileToMove = File(fromDir, "testFile")
+        fileToMove.createNewFile()
+        Os.chmod(fileToMove.absolutePath, 511) // 0777
+        val statFrom = Os.stat(fileToMove.absolutePath)
+
+        MigrationUtils.moveFiles(fromDir, toDir, fileToMove.name)
+
+        val resultFile = File(toDir, fileToMove.name)
+        val stat = Os.stat(resultFile.absolutePath)
+        assertThat(stat.st_mode).isEqualTo(statFrom.st_mode)
+    }
+
+    @Test
+    fun moveFiles_moveMultipleFilesWithPrefix() {
+        val fileToMove1 = File(fromDir, "testFile1")
+        val fileToMove2 = File(fromDir, "testFile2")
+        val fileToKeep = File(fromDir, "keepFile1")
+
+        fileToMove1.createNewFile()
+        fileToMove2.createNewFile()
+        fileToKeep.createNewFile()
+
+        val result = MigrationUtils.moveFiles(fromDir, toDir, "testFile")
+        assertThat(result).isTrue()
+
+        assertThat(fileToMove1.exists()).isFalse()
+        assertThat(fileToMove2.exists()).isFalse()
+        assertThat(fileToKeep.exists()).isTrue()
+
+        val resultFile1 = File(toDir, fileToMove1.name)
+        val resultFile2 = File(toDir, fileToMove2.name)
+        val notCopiedFile = File(toDir, fileToKeep.name)
+
+        assertThat(resultFile1.exists()).isTrue()
+        assertThat(resultFile2.exists()).isTrue()
+        assertThat(notCopiedFile.exists()).isFalse()
+    }
+
+    @Test
+    fun moveFiles_whenSameFromAndTo_keepExistingFile() {
+        val fileToMove = File(fromDir, "testFile")
+        fileToMove.outputStream().use { outputStream ->
+            DataOutputStream(outputStream).use { dataStream ->
+                dataStream.writeInt(42)
+            }
+        }
+
+        val result = MigrationUtils.moveFiles(fromDir, fromDir, fileToMove.name)
+        assertThat(result).isTrue()
+
+        assertThat(fileToMove.exists()).isTrue()
+        val content = fileToMove.inputStream().use { inputStream ->
+            DataInputStream(inputStream).use { dataStream ->
+                dataStream.readInt()
+            }
+        }
+
+        assertThat(content)
+            .isEqualTo(42)
+    }
+
+    @Test
+    fun moveFiles_skipFailedFilesAndReturnFalse() {
+        val fileToMove1 = File(fromDir, "testFile1")
+        val fileToFail = File(fromDir, "testFile2")
+        val fileToMove2 = File(fromDir, "testFile3")
+
+        fileToMove1.createNewFile()
+        fileToFail.mkdir() // to fail copy
+        fileToMove2.createNewFile()
+
+        val result = MigrationUtils.moveFiles(fromDir, toDir, "testFile")
+        assertThat(result).isFalse()
+
+        assertThat(fileToMove1.exists()).isFalse()
+        assertThat(fileToMove2.exists()).isFalse()
+
+        val resultFile1 = File(toDir, fileToMove1.name)
+        val resultFile2 = File(toDir, fileToMove2.name)
+
+        assertThat(resultFile1.exists()).isTrue()
+        assertThat(resultFile2.exists()).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/MigrationUtils.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/MigrationUtils.kt
new file mode 100644
index 0000000..6e3488b
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/MigrationUtils.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.sdkruntime.client.loader.impl
+
+import android.os.Build
+import android.os.FileUtils
+import android.system.ErrnoException
+import android.system.Os
+import android.util.Log
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+internal object MigrationUtils {
+
+    private const val LOG_TAG = "MigrationUtils"
+
+    /**
+     * Try to migrate all files from source to target that match requested prefix.
+     * Skip failed files.
+     *
+     * @return true if all files moved, or false if some fails happened.
+     */
+    fun moveFiles(srcDir: File, destDir: File, prefix: String): Boolean {
+        if (srcDir == destDir) {
+            return true
+        }
+
+        val sourceFiles = srcDir.listFiles { _, name -> name.startsWith(prefix) }
+            ?: emptyArray()
+
+        var hadFails = false
+        for (sourceFile in sourceFiles) {
+            val targetFile = File(destDir, sourceFile.name)
+            Log.d(LOG_TAG, "Migrating $sourceFile to $targetFile")
+            try {
+                copyFile(sourceFile, targetFile)
+                copyPermissions(sourceFile, targetFile)
+                if (!sourceFile.delete()) {
+                    Log.w(LOG_TAG, "Failed to clean up $sourceFile")
+                    hadFails = true
+                }
+            } catch (e: IOException) {
+                Log.w(LOG_TAG, "Failed to migrate $sourceFile", e)
+                hadFails = true
+            } catch (e: ErrnoException) {
+                Log.w(LOG_TAG, "Failed to migrate $sourceFile", e)
+                hadFails = true
+            }
+        }
+        return !hadFails
+    }
+
+    private fun copyFile(sourceFile: File, targetFile: File) {
+        if (targetFile.exists()) {
+            targetFile.delete()
+        }
+        FileInputStream(sourceFile).use { sourceStream ->
+            FileOutputStream(targetFile).use { targetStream ->
+                copy(sourceStream, targetStream)
+                Os.fsync(targetStream.fd)
+            }
+        }
+    }
+
+    private fun copyPermissions(sourceFile: File, targetFile: File) {
+        val stat = Os.stat(sourceFile.absolutePath)
+        Os.chmod(targetFile.absolutePath, stat.st_mode)
+        Os.chown(targetFile.absolutePath, stat.st_uid, stat.st_gid)
+    }
+
+    private fun copy(from: InputStream, to: OutputStream): Long {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            return Api29.copy(from, to)
+        }
+        return from.copyTo(to)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.Q)
+    private object Api29 {
+        @DoNotInline
+        fun copy(from: InputStream, to: OutputStream): Long =
+            FileUtils.copy(from, to)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxedSdkContextCompat.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxedSdkContextCompat.kt
index 4a9fd3b..686e3f7 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxedSdkContextCompat.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxedSdkContextCompat.kt
@@ -17,19 +17,229 @@
 
 import android.content.Context
 import android.content.ContextWrapper
-import androidx.annotation.RestrictTo
+import android.database.DatabaseErrorHandler
+import android.database.sqlite.SQLiteDatabase
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
 
 /**
  * Refers to the context of the SDK loaded locally.
  *
- * @suppress
+ * Supports Per-SDK storage by pointing storage related APIs to folders unique for each SDK.
+ * Where possible maintains same folders hierarchy as for applications by creating folders
+ * inside [getDataDir].
+ * Folders with special permissions or additional logic (caches, etc) created as subfolders of same
+ * application folders.
+ *
+ * SDK Folders hierarchy (from application [getDataDir]):
+ * 1) /cache/RuntimeEnabledSdksData/<sdk_package_name> - cache
+ * 2) /code_cache/RuntimeEnabledSdksData/<sdk_package_name> - code_cache
+ * 3) /no_backup/RuntimeEnabledSdksData/<sdk_package_name> - no_backup
+ * 4) /app_RuntimeEnabledSdksData/<sdk_package_name>/ - SDK Root (data dir)
+ * 5) /app_RuntimeEnabledSdksData/<sdk_package_name>/files - [getFilesDir]
+ * 6) /app_RuntimeEnabledSdksData/<sdk_package_name>/app_<folder_name> - [getDir]
+ * 7) /app_RuntimeEnabledSdksData/<sdk_package_name>/databases - SDK Databases
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 internal class SandboxedSdkContextCompat(
     base: Context,
+    private val sdkPackageName: String,
     private val classLoader: ClassLoader?
 ) : ContextWrapper(base) {
+
+    @RequiresApi(Build.VERSION_CODES.N)
+    override fun createDeviceProtectedStorageContext(): Context {
+        return SandboxedSdkContextCompat(
+            Api24.createDeviceProtectedStorageContext(baseContext),
+            sdkPackageName,
+            classLoader
+        )
+    }
+
+    /**
+     *  Points to <app_data_dir>/app_RuntimeEnabledSdksData/<sdk_package_name>
+     */
+    override fun getDataDir(): File {
+        val sdksDataRoot = baseContext.getDir(
+            SDK_ROOT_FOLDER,
+            Context.MODE_PRIVATE
+        )
+        return ensureDirExists(sdksDataRoot, sdkPackageName)
+    }
+
+    /**
+     *  Points to <app_data_dir>/cache/RuntimeEnabledSdksData/<sdk_package_name>
+     */
+    override fun getCacheDir(): File {
+        val sdksCacheRoot = ensureDirExists(baseContext.cacheDir, SDK_ROOT_FOLDER)
+        return ensureDirExists(sdksCacheRoot, sdkPackageName)
+    }
+
+    /**
+     *  Points to <app_data_dir>/code_cache/RuntimeEnabledSdksData/<sdk_package_name>
+     */
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    override fun getCodeCacheDir(): File {
+        val sdksCodeCacheRoot = ensureDirExists(
+            Api21.codeCacheDir(baseContext),
+            SDK_ROOT_FOLDER
+        )
+        return ensureDirExists(sdksCodeCacheRoot, sdkPackageName)
+    }
+
+    /**
+     *  Points to <app_data_dir>/no_backup/RuntimeEnabledSdksData/<sdk_package_name>
+     */
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    override fun getNoBackupFilesDir(): File {
+        val sdksNoBackupRoot = ensureDirExists(
+            Api21.noBackupFilesDir(baseContext),
+            SDK_ROOT_FOLDER
+        )
+        return ensureDirExists(sdksNoBackupRoot, sdkPackageName)
+    }
+
+    /**
+     *  Points to <app_data_dir>/app_RuntimeEnabledSdksData/<sdk_package_name>/app_<folder_name>
+     *  Prefix required to maintain same hierarchy as for applications - when dir could be
+     *  accessed by both [getDir] and [getDir]/app_<folder_name>.
+     */
+    override fun getDir(name: String, mode: Int): File {
+        val dirName = "app_$name"
+        return ensureDirExists(dataDir, dirName)
+    }
+
+    /**
+     *  Points to <app_data_dir>/app_RuntimeEnabledSdksData/<sdk_package_name>/files
+     */
+    override fun getFilesDir(): File {
+        return ensureDirExists(dataDir, "files")
+    }
+
+    override fun openFileInput(name: String): FileInputStream {
+        val file = makeFilename(filesDir, name)
+        return FileInputStream(file)
+    }
+
+    override fun openFileOutput(name: String, mode: Int): FileOutputStream {
+        val file = makeFilename(filesDir, name)
+        val append = (mode and MODE_APPEND) != 0
+        return FileOutputStream(file, append)
+    }
+
+    override fun deleteFile(name: String): Boolean {
+        val file = makeFilename(filesDir, name)
+        return file.delete()
+    }
+
+    override fun getFileStreamPath(name: String): File {
+        return makeFilename(filesDir, name)
+    }
+
+    override fun fileList(): Array<String> {
+        return listOrEmpty(filesDir)
+    }
+
+    override fun getDatabasePath(name: String): File {
+        if (name[0] == File.separatorChar) {
+            return baseContext.getDatabasePath(name)
+        }
+        val absolutePath = File(getDatabasesDir(), name)
+        return baseContext.getDatabasePath(absolutePath.absolutePath)
+    }
+
+    override fun openOrCreateDatabase(
+        name: String,
+        mode: Int,
+        factory: SQLiteDatabase.CursorFactory?
+    ): SQLiteDatabase {
+        return openOrCreateDatabase(name, mode, factory, null)
+    }
+
+    override fun openOrCreateDatabase(
+        name: String,
+        mode: Int,
+        factory: SQLiteDatabase.CursorFactory?,
+        errorHandler: DatabaseErrorHandler?
+    ): SQLiteDatabase {
+        return baseContext.openOrCreateDatabase(
+            getDatabasePath(name).absolutePath,
+            mode,
+            factory,
+            errorHandler
+        )
+    }
+
+    @RequiresApi(Build.VERSION_CODES.N)
+    override fun moveDatabaseFrom(sourceContext: Context, name: String): Boolean {
+        synchronized(SandboxedSdkContextCompat::class.java) {
+            val source = sourceContext.getDatabasePath(name)
+            val target = getDatabasePath(name)
+            return MigrationUtils.moveFiles(
+                source.parentFile!!,
+                target.parentFile!!,
+                source.name
+            )
+        }
+    }
+
+    override fun deleteDatabase(name: String): Boolean {
+        return baseContext.deleteDatabase(
+            getDatabasePath(name).absolutePath
+        )
+    }
+
+    override fun databaseList(): Array<String> {
+        return listOrEmpty(getDatabasesDir())
+    }
     override fun getClassLoader(): ClassLoader? {
         return classLoader
     }
+
+    private fun getDatabasesDir(): File =
+        ensureDirExists(dataDir, "databases")
+
+    private fun listOrEmpty(dir: File?): Array<String> {
+        return dir?.list() ?: emptyArray()
+    }
+
+    private fun makeFilename(parent: File, name: String): File {
+        if (name.indexOf(File.separatorChar) >= 0) {
+            throw IllegalArgumentException(
+                "File $name contains a path separator"
+            )
+        }
+        return File(parent, name)
+    }
+
+    private fun ensureDirExists(parent: File, dirName: String): File {
+        val dir = File(parent, dirName)
+        if (!dir.exists()) {
+            dir.mkdir()
+        }
+        return dir
+    }
+
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    private object Api21 {
+        @DoNotInline
+        fun codeCacheDir(context: Context): File = context.codeCacheDir
+
+        @DoNotInline
+        fun noBackupFilesDir(context: Context): File = context.noBackupFilesDir
+    }
+
+    @RequiresApi(Build.VERSION_CODES.N)
+    private object Api24 {
+        @DoNotInline
+        fun createDeviceProtectedStorageContext(context: Context): Context =
+            context.createDeviceProtectedStorageContext()
+    }
+
+    private companion object {
+        private const val SDK_ROOT_FOLDER = "RuntimeEnabledSdksData"
+    }
 }
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SdkProviderV1.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SdkProviderV1.kt
index ce51c0ef..4a6d959 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SdkProviderV1.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SdkProviderV1.kt
@@ -175,7 +175,11 @@
                 LoadSdkCompatExceptionBuilderV1.create(classLoader)
 
             val sdkProvider = sdkProviderClass.getConstructor().newInstance()
-            val sandboxedSdkContextCompat = SandboxedSdkContextCompat(appContext, classLoader)
+            val sandboxedSdkContextCompat = SandboxedSdkContextCompat(
+                appContext,
+                sdkConfig.packageName,
+                classLoader
+            )
             attachContextMethod.invoke(sdkProvider, sandboxedSdkContextCompat)
 
             return SdkProviderV1(
diff --git a/privacysandbox/tools/tools-apipackager/build.gradle b/privacysandbox/tools/tools-apipackager/build.gradle
index 006779b..19089d3 100644
--- a/privacysandbox/tools/tools-apipackager/build.gradle
+++ b/privacysandbox/tools/tools-apipackager/build.gradle
@@ -15,8 +15,6 @@
  */
 
 import androidx.build.LibraryType
-import androidx.build.SdkHelperKt
-import androidx.build.SupportConfig
 
 plugins {
     id("AndroidXPlugin")
@@ -37,13 +35,6 @@
     testImplementation(project(":room:room-compiler-processing-testing"))
     testImplementation(libs.junit)
     testImplementation(libs.truth)
-
-    // TODO(b/281638337): Remove below dependency once SdkActivityLauncher stubs are removed
-    // Include android jar for compilation of generated sources.
-    testImplementation(fileTree(
-            dir: "${SdkHelperKt.getSdkPath(project)}/platforms/$SupportConfig.COMPILE_SDK_VERSION/",
-            include: "android.jar"
-    ))
 }
 
 androidx {
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
index eb4b128..ba20a08 100644
--- 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
@@ -208,7 +208,7 @@
 
     /** Compiles the given source file and returns a classpath with the results. */
     private fun compileAndReturnUnzippedPackagedClasspath(source: Source): File {
-        val result = compileAll(listOf(source), includeLibraryStubs = false)
+        val result = compileAll(listOf(source))
         assertThat(result).succeeds()
         assertThat(result.outputClasspath).hasSize(1)
 
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 84dd495..46db8fff 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
@@ -41,15 +41,12 @@
         extraClasspath: List<File> = emptyList(),
         symbolProcessorProviders: List<SymbolProcessorProvider> = emptyList(),
         processorOptions: Map<String, String> = emptyMap(),
-        includeLibraryStubs: Boolean = true,
     ): TestCompilationResult {
         val tempDir = Files.createTempDirectory("compile").toFile().also { it.deleteOnExit() }
-        // TODO(b/281638337): Remove library stubs once SdkActivityLauncher is upstreamed
-        val fullSources = sources + if (includeLibraryStubs) libraryStubs else emptyList()
         return compile(
             tempDir,
             TestCompilationArguments(
-                sources = fullSources,
+                sources = sources,
                 classpath = extraClasspath,
                 symbolProcessorProviders = symbolProcessorProviders,
                 processorOptions = processorOptions,
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/LibraryStubs.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/LibraryStubs.kt
deleted file mode 100644
index f0caa9e..0000000
--- a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/LibraryStubs.kt
+++ /dev/null
@@ -1,350 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.privacysandbox.tools.testing
-
-import androidx.room.compiler.processing.util.Source
-
-private val syntheticUiLibraryStubs = listOf(
-    Source.kotlin(
-        "androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt", """
-        |package androidx.privacysandbox.ui.core
-        |
-        |import android.os.IBinder
-        |
-        |interface SdkActivityLauncher {
-        |    suspend fun launchSdkActivity(sdkActivityHandlerToken: IBinder): Boolean
-        |}
-        |""".trimMargin()
-    ),
-    Source.kotlin(
-        "androidx/privacysandbox/ui/client/SdkActivityLaunchers.kt", """
-        |@file:JvmName("SdkActivityLaunchers")
-        |
-        |package androidx.privacysandbox.ui.client
-        |
-        |import android.os.Bundle
-        |import androidx.privacysandbox.ui.core.SdkActivityLauncher
-        |
-        |fun SdkActivityLauncher.toLauncherInfo(): Bundle {
-        |    TODO("Stub!")
-        |}
-        |""".trimMargin()
-    ),
-    Source.kotlin(
-        "androidx/privacysandbox/ui/provider/SdkActivityLauncherFactory.kt", """
-        |package androidx.privacysandbox.ui.provider
-        |
-        |import android.os.Bundle
-        |import androidx.privacysandbox.ui.core.SdkActivityLauncher
-        |
-        |object SdkActivityLauncherFactory {
-        |
-        |    @JvmStatic
-        |    @Suppress("UNUSED_PARAMETER")
-        |    fun fromLauncherInfo(launcherInfo: Bundle): SdkActivityLauncher {
-        |        TODO("Stub!")
-        |    }
-        |}""".trimMargin()
-    ),
-    Source.kotlin(
-        "androidx/core/os/BundleCompat.kt", """
-        |package androidx.core.os
-        |
-        |import android.os.IBinder
-        |import android.os.Bundle
-        |
-        |object BundleCompat {
-        |    @Suppress("UNUSED_PARAMETER")
-        |    fun getBinder(bundle: Bundle, key: String?): IBinder? {
-        |        TODO("Stub!")
-        |    }
-        |}
-        |""".trimMargin()
-    ),
-)
-
-private val syntheticAidlGeneratedCode = listOf(
-    Source.java(
-        "androidx/privacysandbox/ui/core/ISdkActivityLauncher", """
-        |package androidx.privacysandbox.ui.core;
-        |/** @hide */
-        |public interface ISdkActivityLauncher extends android.os.IInterface
-        |{
-        |  /** Default implementation for ISdkActivityLauncher. */
-        |  public static class Default implements androidx.privacysandbox.ui.core.ISdkActivityLauncher
-        |  {
-        |    @Override public void launchSdkActivity(android.os.IBinder sdkActivityHandlerToken, androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback callback) throws android.os.RemoteException
-        |    {
-        |    }
-        |    @Override
-        |    public android.os.IBinder asBinder() {
-        |      return null;
-        |    }
-        |  }
-        |  /** Local-side IPC implementation stub class. */
-        |  public static abstract class Stub extends android.os.Binder implements androidx.privacysandbox.ui.core.ISdkActivityLauncher
-        |  {
-        |    /** Construct the stub at attach it to the interface. */
-        |    public Stub()
-        |    {
-        |      this.attachInterface(this, DESCRIPTOR);
-        |    }
-        |    /**
-        |     * Cast an IBinder object into an androidx.privacysandbox.ui.core.ISdkActivityLauncher interface,
-        |     * generating a proxy if needed.
-        |     */
-        |    public static androidx.privacysandbox.ui.core.ISdkActivityLauncher asInterface(android.os.IBinder obj)
-        |    {
-        |      if ((obj==null)) {
-        |        return null;
-        |      }
-        |      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
-        |      if (((iin!=null)&&(iin instanceof androidx.privacysandbox.ui.core.ISdkActivityLauncher))) {
-        |        return ((androidx.privacysandbox.ui.core.ISdkActivityLauncher)iin);
-        |      }
-        |      return new androidx.privacysandbox.ui.core.ISdkActivityLauncher.Stub.Proxy(obj);
-        |    }
-        |    @Override public android.os.IBinder asBinder()
-        |    {
-        |      return this;
-        |    }
-        |    @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
-        |    {
-        |      java.lang.String descriptor = DESCRIPTOR;
-        |      if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
-        |        data.enforceInterface(descriptor);
-        |      }
-        |      switch (code)
-        |      {
-        |        case INTERFACE_TRANSACTION:
-        |        {
-        |          reply.writeString(descriptor);
-        |          return true;
-        |        }
-        |      }
-        |      switch (code)
-        |      {
-        |        case TRANSACTION_launchSdkActivity:
-        |        {
-        |          android.os.IBinder _arg0;
-        |          _arg0 = data.readStrongBinder();
-        |          androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback _arg1;
-        |          _arg1 = androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback.Stub.asInterface(data.readStrongBinder());
-        |          this.launchSdkActivity(_arg0, _arg1);
-        |          break;
-        |        }
-        |        default:
-        |        {
-        |          return super.onTransact(code, data, reply, flags);
-        |        }
-        |      }
-        |      return true;
-        |    }
-        |    private static class Proxy implements androidx.privacysandbox.ui.core.ISdkActivityLauncher
-        |    {
-        |      private android.os.IBinder mRemote;
-        |      Proxy(android.os.IBinder remote)
-        |      {
-        |        mRemote = remote;
-        |      }
-        |      @Override public android.os.IBinder asBinder()
-        |      {
-        |        return mRemote;
-        |      }
-        |      public java.lang.String getInterfaceDescriptor()
-        |      {
-        |        return DESCRIPTOR;
-        |      }
-        |      @Override public void launchSdkActivity(android.os.IBinder sdkActivityHandlerToken, androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback callback) throws android.os.RemoteException
-        |      {
-        |        android.os.Parcel _data = android.os.Parcel.obtain();
-        |        try {
-        |          _data.writeInterfaceToken(DESCRIPTOR);
-        |          _data.writeStrongBinder(sdkActivityHandlerToken);
-        |          _data.writeStrongInterface(callback);
-        |          boolean _status = mRemote.transact(Stub.TRANSACTION_launchSdkActivity, _data, null, android.os.IBinder.FLAG_ONEWAY);
-        |        }
-        |        finally {
-        |          _data.recycle();
-        |        }
-        |      }
-        |    }
-        |    static final int TRANSACTION_launchSdkActivity = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
-        |  }
-        |  public static final java.lang.String DESCRIPTOR = "androidx.privacysandbox.ui.core.ISdkActivityLauncher";
-        |  public void launchSdkActivity(android.os.IBinder sdkActivityHandlerToken, androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback callback) throws android.os.RemoteException;
-        |}""".trimMargin()
-    ),
-    Source.java(
-        "androidx/privacysandbox/ui/core/ISdkActivityLauncherCallback", """
-        |package androidx.privacysandbox.ui.core;
-        |/** @hide */
-        |public interface ISdkActivityLauncherCallback extends android.os.IInterface
-        |{
-        |  /** Default implementation for ISdkActivityLauncherCallback. */
-        |  public static class Default implements androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback
-        |  {
-        |    @Override public void onLaunchAccepted(android.os.IBinder sdkActivityHandlerToken) throws android.os.RemoteException
-        |    {
-        |    }
-        |    @Override public void onLaunchRejected(android.os.IBinder sdkActivityHandlerToken) throws android.os.RemoteException
-        |    {
-        |    }
-        |    @Override public void onLaunchError(java.lang.String message) throws android.os.RemoteException
-        |    {
-        |    }
-        |    @Override
-        |    public android.os.IBinder asBinder() {
-        |      return null;
-        |    }
-        |  }
-        |  /** Local-side IPC implementation stub class. */
-        |  public static abstract class Stub extends android.os.Binder implements androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback
-        |  {
-        |    /** Construct the stub at attach it to the interface. */
-        |    public Stub()
-        |    {
-        |      this.attachInterface(this, DESCRIPTOR);
-        |    }
-        |    /**
-        |     * Cast an IBinder object into an androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback interface,
-        |     * generating a proxy if needed.
-        |     */
-        |    public static androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback asInterface(android.os.IBinder obj)
-        |    {
-        |      if ((obj==null)) {
-        |        return null;
-        |      }
-        |      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
-        |      if (((iin!=null)&&(iin instanceof androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback))) {
-        |        return ((androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback)iin);
-        |      }
-        |      return new androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback.Stub.Proxy(obj);
-        |    }
-        |    @Override public android.os.IBinder asBinder()
-        |    {
-        |      return this;
-        |    }
-        |    @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
-        |    {
-        |      java.lang.String descriptor = DESCRIPTOR;
-        |      if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
-        |        data.enforceInterface(descriptor);
-        |      }
-        |      switch (code)
-        |      {
-        |        case INTERFACE_TRANSACTION:
-        |        {
-        |          reply.writeString(descriptor);
-        |          return true;
-        |        }
-        |      }
-        |      switch (code)
-        |      {
-        |        case TRANSACTION_onLaunchAccepted:
-        |        {
-        |          android.os.IBinder _arg0;
-        |          _arg0 = data.readStrongBinder();
-        |          this.onLaunchAccepted(_arg0);
-        |          break;
-        |        }
-        |        case TRANSACTION_onLaunchRejected:
-        |        {
-        |          android.os.IBinder _arg0;
-        |          _arg0 = data.readStrongBinder();
-        |          this.onLaunchRejected(_arg0);
-        |          break;
-        |        }
-        |        case TRANSACTION_onLaunchError:
-        |        {
-        |          java.lang.String _arg0;
-        |          _arg0 = data.readString();
-        |          this.onLaunchError(_arg0);
-        |          break;
-        |        }
-        |        default:
-        |        {
-        |          return super.onTransact(code, data, reply, flags);
-        |        }
-        |      }
-        |      return true;
-        |    }
-        |    private static class Proxy implements androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback
-        |    {
-        |      private android.os.IBinder mRemote;
-        |      Proxy(android.os.IBinder remote)
-        |      {
-        |        mRemote = remote;
-        |      }
-        |      @Override public android.os.IBinder asBinder()
-        |      {
-        |        return mRemote;
-        |      }
-        |      public java.lang.String getInterfaceDescriptor()
-        |      {
-        |        return DESCRIPTOR;
-        |      }
-        |      @Override public void onLaunchAccepted(android.os.IBinder sdkActivityHandlerToken) throws android.os.RemoteException
-        |      {
-        |        android.os.Parcel _data = android.os.Parcel.obtain();
-        |        try {
-        |          _data.writeInterfaceToken(DESCRIPTOR);
-        |          _data.writeStrongBinder(sdkActivityHandlerToken);
-        |          boolean _status = mRemote.transact(Stub.TRANSACTION_onLaunchAccepted, _data, null, android.os.IBinder.FLAG_ONEWAY);
-        |        }
-        |        finally {
-        |          _data.recycle();
-        |        }
-        |      }
-        |      @Override public void onLaunchRejected(android.os.IBinder sdkActivityHandlerToken) throws android.os.RemoteException
-        |      {
-        |        android.os.Parcel _data = android.os.Parcel.obtain();
-        |        try {
-        |          _data.writeInterfaceToken(DESCRIPTOR);
-        |          _data.writeStrongBinder(sdkActivityHandlerToken);
-        |          boolean _status = mRemote.transact(Stub.TRANSACTION_onLaunchRejected, _data, null, android.os.IBinder.FLAG_ONEWAY);
-        |        }
-        |        finally {
-        |          _data.recycle();
-        |        }
-        |      }
-        |      @Override public void onLaunchError(java.lang.String message) throws android.os.RemoteException
-        |      {
-        |        android.os.Parcel _data = android.os.Parcel.obtain();
-        |        try {
-        |          _data.writeInterfaceToken(DESCRIPTOR);
-        |          _data.writeString(message);
-        |          boolean _status = mRemote.transact(Stub.TRANSACTION_onLaunchError, _data, null, android.os.IBinder.FLAG_ONEWAY);
-        |        }
-        |        finally {
-        |          _data.recycle();
-        |        }
-        |      }
-        |    }
-        |    static final int TRANSACTION_onLaunchAccepted = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
-        |    static final int TRANSACTION_onLaunchRejected = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
-        |    static final int TRANSACTION_onLaunchError = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
-        |  }
-        |  public static final java.lang.String DESCRIPTOR = "androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback";
-        |  public void onLaunchAccepted(android.os.IBinder sdkActivityHandlerToken) throws android.os.RemoteException;
-        |  public void onLaunchRejected(android.os.IBinder sdkActivityHandlerToken) throws android.os.RemoteException;
-        |  public void onLaunchError(java.lang.String message) throws android.os.RemoteException;
-        |}""".trimMargin()
-    ),
-)
-
-val libraryStubs = syntheticUiLibraryStubs + syntheticAidlGeneratedCode
\ No newline at end of file
diff --git a/tracing/tracing-perfetto-common/api/current.txt b/tracing/tracing-perfetto-common/api/current.txt
index 98f9ffc7..9aa8915 100644
--- a/tracing/tracing-perfetto-common/api/current.txt
+++ b/tracing/tracing-perfetto-common/api/current.txt
@@ -1,13 +1,21 @@
 // Signature format: 4.0
-package androidx.tracing.perfetto {
+package androidx.tracing.perfetto.handshake {
 
   public final class PerfettoSdkHandshake {
     ctor public PerfettoSdkHandshake(String targetPackage, kotlin.jvm.functions.Function1<? super java.lang.String,? extends java.util.Map<java.lang.String,java.lang.String>> parseJsonMap, kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.String> executeShellCommand);
-    method public androidx.tracing.perfetto.PerfettoSdkHandshake.EnableTracingResponse enableTracingColdStart(kotlin.jvm.functions.Function0<kotlin.Unit> killAppProcess, androidx.tracing.perfetto.PerfettoSdkHandshake.LibrarySource? librarySource);
-    method public androidx.tracing.perfetto.PerfettoSdkHandshake.EnableTracingResponse enableTracingImmediate(optional androidx.tracing.perfetto.PerfettoSdkHandshake.LibrarySource? librarySource);
+    method public androidx.tracing.perfetto.handshake.protocol.EnableTracingResponse enableTracingColdStart(kotlin.jvm.functions.Function0<kotlin.Unit> killAppProcess, androidx.tracing.perfetto.handshake.PerfettoSdkHandshake.LibrarySource? librarySource);
+    method public androidx.tracing.perfetto.handshake.protocol.EnableTracingResponse enableTracingImmediate(optional androidx.tracing.perfetto.handshake.PerfettoSdkHandshake.LibrarySource? librarySource);
   }
 
-  public static final class PerfettoSdkHandshake.EnableTracingResponse {
+  public static final class PerfettoSdkHandshake.LibrarySource {
+    ctor public PerfettoSdkHandshake.LibrarySource(java.io.File libraryZip, java.io.File tempDirectory, kotlin.jvm.functions.Function2<? super java.io.File,? super java.io.File,kotlin.Unit> moveLibFileFromTmpDirToAppDir);
+  }
+
+}
+
+package androidx.tracing.perfetto.handshake.protocol {
+
+  public final class EnableTracingResponse {
     method public int getExitCode();
     method public String? getMessage();
     method public String? getRequiredVersion();
@@ -16,12 +24,8 @@
     property public final String? requiredVersion;
   }
 
-  public static final class PerfettoSdkHandshake.LibrarySource {
-    ctor public PerfettoSdkHandshake.LibrarySource(java.io.File libraryZip, java.io.File tempDirectory, kotlin.jvm.functions.Function2<? super java.io.File,? super java.io.File,kotlin.Unit> moveLibFileFromTmpDirToAppDir);
-  }
-
-  public static final class PerfettoSdkHandshake.ResponseExitCodes {
-    field public static final androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes INSTANCE;
+  public final class ResponseExitCodes {
+    field public static final androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes INSTANCE;
     field public static final int RESULT_CODE_ALREADY_ENABLED = 2; // 0x2
     field public static final int RESULT_CODE_CANCELLED = 0; // 0x0
     field public static final int RESULT_CODE_ERROR_BINARY_MISSING = 11; // 0xb
diff --git a/tracing/tracing-perfetto-common/api/restricted_current.txt b/tracing/tracing-perfetto-common/api/restricted_current.txt
index 98f9ffc7..9aa8915 100644
--- a/tracing/tracing-perfetto-common/api/restricted_current.txt
+++ b/tracing/tracing-perfetto-common/api/restricted_current.txt
@@ -1,13 +1,21 @@
 // Signature format: 4.0
-package androidx.tracing.perfetto {
+package androidx.tracing.perfetto.handshake {
 
   public final class PerfettoSdkHandshake {
     ctor public PerfettoSdkHandshake(String targetPackage, kotlin.jvm.functions.Function1<? super java.lang.String,? extends java.util.Map<java.lang.String,java.lang.String>> parseJsonMap, kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.String> executeShellCommand);
-    method public androidx.tracing.perfetto.PerfettoSdkHandshake.EnableTracingResponse enableTracingColdStart(kotlin.jvm.functions.Function0<kotlin.Unit> killAppProcess, androidx.tracing.perfetto.PerfettoSdkHandshake.LibrarySource? librarySource);
-    method public androidx.tracing.perfetto.PerfettoSdkHandshake.EnableTracingResponse enableTracingImmediate(optional androidx.tracing.perfetto.PerfettoSdkHandshake.LibrarySource? librarySource);
+    method public androidx.tracing.perfetto.handshake.protocol.EnableTracingResponse enableTracingColdStart(kotlin.jvm.functions.Function0<kotlin.Unit> killAppProcess, androidx.tracing.perfetto.handshake.PerfettoSdkHandshake.LibrarySource? librarySource);
+    method public androidx.tracing.perfetto.handshake.protocol.EnableTracingResponse enableTracingImmediate(optional androidx.tracing.perfetto.handshake.PerfettoSdkHandshake.LibrarySource? librarySource);
   }
 
-  public static final class PerfettoSdkHandshake.EnableTracingResponse {
+  public static final class PerfettoSdkHandshake.LibrarySource {
+    ctor public PerfettoSdkHandshake.LibrarySource(java.io.File libraryZip, java.io.File tempDirectory, kotlin.jvm.functions.Function2<? super java.io.File,? super java.io.File,kotlin.Unit> moveLibFileFromTmpDirToAppDir);
+  }
+
+}
+
+package androidx.tracing.perfetto.handshake.protocol {
+
+  public final class EnableTracingResponse {
     method public int getExitCode();
     method public String? getMessage();
     method public String? getRequiredVersion();
@@ -16,12 +24,8 @@
     property public final String? requiredVersion;
   }
 
-  public static final class PerfettoSdkHandshake.LibrarySource {
-    ctor public PerfettoSdkHandshake.LibrarySource(java.io.File libraryZip, java.io.File tempDirectory, kotlin.jvm.functions.Function2<? super java.io.File,? super java.io.File,kotlin.Unit> moveLibFileFromTmpDirToAppDir);
-  }
-
-  public static final class PerfettoSdkHandshake.ResponseExitCodes {
-    field public static final androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes INSTANCE;
+  public final class ResponseExitCodes {
+    field public static final androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes INSTANCE;
     field public static final int RESULT_CODE_ALREADY_ENABLED = 2; // 0x2
     field public static final int RESULT_CODE_CANCELLED = 0; // 0x0
     field public static final int RESULT_CODE_ERROR_BINARY_MISSING = 11; // 0xb
diff --git a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoSdkHandshake.kt b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoSdkHandshake.kt
deleted file mode 100644
index 9f1a06b..0000000
--- a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoSdkHandshake.kt
+++ /dev/null
@@ -1,348 +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.tracing.perfetto
-
-import androidx.annotation.IntDef
-import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
-import androidx.tracing.perfetto.PerfettoSdkHandshake.RequestKeys.ACTION_ENABLE_TRACING
-import androidx.tracing.perfetto.PerfettoSdkHandshake.RequestKeys.ACTION_ENABLE_TRACING_COLD_START
-import androidx.tracing.perfetto.PerfettoSdkHandshake.RequestKeys.KEY_PATH
-import androidx.tracing.perfetto.PerfettoSdkHandshake.RequestKeys.KEY_PERSISTENT
-import androidx.tracing.perfetto.PerfettoSdkHandshake.RequestKeys.RECEIVER_CLASS_NAME
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseKeys.KEY_EXIT_CODE
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseKeys.KEY_MESSAGE
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseKeys.KEY_REQUIRED_VERSION
-import java.io.File
-import java.lang.StringBuilder
-
-/**
- * Handshake implementation allowing to enable Perfetto SDK tracing in an app that enables it.
- *
- * @param targetPackage package name of the target app
- * @param parseJsonMap function parsing a flat map in a JSON format into a `Map<String, String>`
- * e.g. `"{ 'key 1': 'value 1', 'key 2': 'value 2' }"` ->
- * `mapOf("key 1" to "value 1", "key 2" to "value 2")`
- * @param executeShellCommand function allowing to execute `adb shell` commands on the target device
- *
- * For error handling, note that [parseJsonMap] and [executeShellCommand] will be called on the same
- * thread as [enableTracingImmediate] and [enableTracingColdStart].
- */
-public class PerfettoSdkHandshake(
-    private val targetPackage: String,
-    private val parseJsonMap: (jsonString: String) -> Map<String, String>,
-    private val executeShellCommand: ShellCommandExecutor
-) {
-    /**
-     * Attempts to enable tracing in an app. It will wake up (or start) the app process, so it will
-     * act as warm/hot tracing. For cold tracing see [enableTracingColdStart]
-     *
-     * Note: if the app process is not running, it will be launched making the method a bad choice
-     * for cold tracing (use [enableTracingColdStart] instead.
-     *
-     * @param librarySource optional AAR or an APK containing `libtracing_perfetto.so`
-     */
-    public fun enableTracingImmediate(
-        librarySource: LibrarySource? = null
-    ): EnableTracingResponse {
-        val libPath = librarySource?.run {
-            PerfettoSdkSideloader(targetPackage).sideloadFromZipFile(
-                libraryZip,
-                tempDirectory,
-                executeShellCommand,
-                moveLibFileFromTmpDirToAppDir
-            )
-        }
-        return sendEnableTracingBroadcast(libPath, coldStart = false)
-    }
-
-    /**
-     * Attempts to prepare cold startup tracing in an app.
-     *
-     * @param killAppProcess function responsible for terminating the app process (no-op if the
-     * process is already terminated)
-     * @param librarySource optional AAR or an APK containing `libtracing_perfetto.so`
-     */
-    public fun enableTracingColdStart(
-        killAppProcess: () -> Unit,
-        librarySource: LibrarySource?
-    ): EnableTracingResponse {
-        // sideload the `libtracing_perfetto.so` file if applicable
-        val libPath = librarySource?.run {
-            PerfettoSdkSideloader(targetPackage).sideloadFromZipFile(
-                libraryZip,
-                tempDirectory,
-                executeShellCommand,
-                moveLibFileFromTmpDirToAppDir
-            )
-        }
-
-        // ensure a clean start (e.g. in case tracing is already enabled)
-        killAppProcess()
-
-        // verify (by performing a regular handshake) that we can enable tracing at app startup
-        val response = sendEnableTracingBroadcast(libPath, coldStart = true, persistent = false)
-        if (response.exitCode == ResponseExitCodes.RESULT_CODE_SUCCESS) {
-            // terminate the app process (that we woke up by issuing a broadcast earlier)
-            killAppProcess()
-        }
-
-        return response
-    }
-
-    private fun sendEnableTracingBroadcast(
-        libPath: File? = null,
-        coldStart: Boolean,
-        persistent: Boolean? = null
-    ): EnableTracingResponse {
-        val action = if (coldStart) ACTION_ENABLE_TRACING_COLD_START else ACTION_ENABLE_TRACING
-        val commandBuilder = StringBuilder("am broadcast -a $action")
-        if (persistent != null) commandBuilder.append(" --es $KEY_PERSISTENT $persistent")
-        if (libPath != null) commandBuilder.append(" --es $KEY_PATH $libPath")
-        commandBuilder.append(" $targetPackage/$RECEIVER_CLASS_NAME")
-
-        val rawResponse = executeShellCommand(commandBuilder.toString())
-
-        val response = try {
-            parseResponse(rawResponse)
-        } catch (e: IllegalArgumentException) {
-            val message = "Exception occurred while trying to parse a response." +
-                " Error: ${e.message}. Raw response: $rawResponse."
-            EnableTracingResponse(ResponseExitCodes.RESULT_CODE_ERROR_OTHER, null, message)
-        }
-        return response
-    }
-
-    private fun parseResponse(rawResponse: String): EnableTracingResponse {
-        val line = rawResponse
-            .split(Regex("\r?\n"))
-            .firstOrNull { it.contains("Broadcast completed: result=") }
-            ?: throw IllegalArgumentException("Cannot parse: $rawResponse")
-
-        if (line == "Broadcast completed: result=0") return EnableTracingResponse(
-            ResponseExitCodes.RESULT_CODE_CANCELLED, null, null
-        )
-
-        val matchResult =
-            Regex("Broadcast completed: (result=.*?)(, data=\".*?\")?(, extras: .*)?")
-                .matchEntire(line)
-                ?: throw IllegalArgumentException("Cannot parse: $rawResponse")
-
-        val broadcastResponseCode = matchResult
-            .groups[1]
-            ?.value
-            ?.substringAfter("result=")
-            ?.toIntOrNull()
-
-        val dataString = matchResult
-            .groups
-            .firstOrNull { it?.value?.startsWith(", data=") ?: false }
-            ?.value
-            ?.substringAfter(", data=\"")
-            ?.dropLast(1)
-            ?: throw IllegalArgumentException("Cannot parse: $rawResponse. " +
-                "Unable to detect 'data=' section."
-            )
-
-        val dataMap = parseJsonMap(dataString)
-        val response = EnableTracingResponse(
-            dataMap[KEY_EXIT_CODE]?.toInt()
-                ?: throw IllegalArgumentException("Response missing $KEY_EXIT_CODE value"),
-            dataMap[KEY_REQUIRED_VERSION]
-                ?: throw IllegalArgumentException("Response missing $KEY_REQUIRED_VERSION value"),
-            dataMap[KEY_MESSAGE]
-        )
-
-        if (broadcastResponseCode != response.exitCode) {
-            throw IllegalStateException(
-                "Cannot parse: $rawResponse. Exit code " +
-                    "not matching broadcast exit code."
-            )
-        }
-
-        return response
-    }
-
-    /**
-    * @param libraryZip either an AAR or an APK containing `libtracing_perfetto.so`
-    * @param tempDirectory a directory directly accessible to the caller process (used for
-     * extraction of the binaries from the zip)
-    * @param moveLibFileFromTmpDirToAppDir a function capable of moving the binary file from
-    * the [tempDirectory] to an app accessible folder
-    */
-    // TODO(245426369): consider moving to a factory pattern for constructing these and refer to
-    //  this one as `aarLibrarySource` and `apkLibrarySource`
-    public class LibrarySource @Suppress("StreamFiles") constructor(
-        internal val libraryZip: File,
-        internal val tempDirectory: File,
-        internal val moveLibFileFromTmpDirToAppDir: FileMover
-    )
-
-    @RestrictTo(LIBRARY_GROUP)
-    public object RequestKeys {
-        public const val RECEIVER_CLASS_NAME: String = "androidx.tracing.perfetto.TracingReceiver"
-
-        /**
-         * Request to enable tracing in an app.
-         *
-         * The action is performed straight away allowing for warm / hot tracing. For cold start
-         * tracing see [ACTION_ENABLE_TRACING_COLD_START]
-         *
-         * Request can include [KEY_PATH] as an optional extra.
-         *
-         * Response to the request is a JSON string (to allow for CLI support) with the following:
-         * - [ResponseKeys.KEY_EXIT_CODE] (always)
-         * - [ResponseKeys.KEY_REQUIRED_VERSION] (always)
-         * - [ResponseKeys.KEY_MESSAGE] (optional)
-         */
-        public const val ACTION_ENABLE_TRACING: String =
-            "androidx.tracing.perfetto.action.ENABLE_TRACING"
-
-        /**
-         * Request to enable cold start tracing in an app.
-         *
-         * For warm / hot tracing, see [ACTION_ENABLE_TRACING].
-         *
-         * The action must be performed in the following order, otherwise its effects are
-         * unspecified:
-         * - the app process must be killed before performing the action
-         * - the action must then follow
-         * - the app process must be killed after performing the action
-         *
-         * Request can include [KEY_PATH] as an optional extra.
-         * Request can include [KEY_PERSISTENT] as an optional extra.
-         *
-         * Response to the request is a JSON string (to allow for CLI support) with the following:
-         * - [ResponseKeys.KEY_EXIT_CODE] (always)
-         * - [ResponseKeys.KEY_REQUIRED_VERSION] (always)
-         * - [ResponseKeys.KEY_MESSAGE] (optional)
-         */
-        public const val ACTION_ENABLE_TRACING_COLD_START: String =
-            "androidx.tracing.perfetto.action.ENABLE_TRACING_COLD_START"
-
-        /**
-         * Request to disable cold start tracing (previously enabled with
-         * [ACTION_ENABLE_TRACING_COLD_START]).
-         *
-         * The action is particularly useful when cold start tracing was enabled in
-         * [KEY_PERSISTENT] mode.
-         *
-         * The action must be performed in the following order, otherwise its effects are
-         * unspecified:
-         * - the app process must be killed before performing the action
-         * - the action must then follow
-         * - the app process must be killed after performing the action
-         *
-         * Request can include [KEY_PATH] as an optional extra.
-         * Request can include [KEY_PERSISTENT] as an optional extra.
-         *
-         * Response to the request is a JSON string (to allow for CLI support) with the following:
-         * - [ResponseKeys.KEY_EXIT_CODE] (always)
-         */
-        public const val ACTION_DISABLE_TRACING_COLD_START: String =
-            "androidx.tracing.perfetto.action.DISABLE_TRACING_COLD_START"
-
-        /** Path to tracing native binary file */
-        public const val KEY_PATH: String = "path"
-
-        /**
-         * Boolean flag to signify whether the operation should be persistent between runs
-         * (or only performed once).
-         *
-         * Applies to [ACTION_ENABLE_TRACING_COLD_START]
-         */
-        public const val KEY_PERSISTENT: String = "persistent"
-    }
-
-    @RestrictTo(LIBRARY_GROUP)
-    public object ResponseKeys {
-        /** Exit code as listed in [ResponseExitCodes]. */
-        public const val KEY_EXIT_CODE: String = "exitCode"
-
-        /**
-         * Required version of the binaries. Java and binary library versions have to match to
-         * ensure compatibility. In the Maven format, e.g. 1.2.3-beta01.
-         */
-        public const val KEY_REQUIRED_VERSION: String = "requiredVersion"
-
-        /**
-         * Message string that gives more information about the response, e.g. recovery steps
-         * if applicable.
-         */
-        public const val KEY_MESSAGE: String = "message"
-    }
-
-    public object ResponseExitCodes {
-        /**
-         * Indicates that the broadcast resulted in `result=0`, which is an equivalent
-         * of [android.app.Activity.RESULT_CANCELED].
-         *
-         * This most likely means that the app does not expose a [PerfettoSdkHandshake] compatible
-         * receiver.
-         */
-        @Suppress("KDocUnresolvedReference")
-        public const val RESULT_CODE_CANCELLED: Int = 0
-
-        public const val RESULT_CODE_SUCCESS: Int = 1
-        public const val RESULT_CODE_ALREADY_ENABLED: Int = 2
-
-        /**
-         * Required version described in [EnableTracingResponse.requiredVersion].
-         * A follow-up [enableTracingImmediate] request expected with binaries to sideload specified.
-         */
-        public const val RESULT_CODE_ERROR_BINARY_MISSING: Int = 11
-
-        /** Required version described in [EnableTracingResponse.requiredVersion]. */
-        public const val RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH: Int = 12
-
-        /**
-         * Could be a result of a stale version of the binary cached locally.
-         * Retrying with a freshly downloaded library likely to fix the issue.
-         * More specific information in [EnableTracingResponse.message]
-         */
-        public const val RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR: Int = 13
-
-        /** More specific information in [EnableTracingResponse.message] */
-        public const val RESULT_CODE_ERROR_OTHER: Int = 99
-    }
-
-    @Retention(AnnotationRetention.SOURCE)
-    @IntDef(
-        ResponseExitCodes.RESULT_CODE_CANCELLED,
-        ResponseExitCodes.RESULT_CODE_SUCCESS,
-        ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED,
-        ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING,
-        ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH,
-        ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR,
-        ResponseExitCodes.RESULT_CODE_ERROR_OTHER
-    )
-    private annotation class EnableTracingResultCode
-
-    public class EnableTracingResponse @RestrictTo(LIBRARY_GROUP) constructor(
-        @EnableTracingResultCode public val exitCode: Int,
-
-        /**
-         * This can be `null` iff we cannot communicate with the broadcast receiver of the target
-         * process (e.g. app does not offer Perfetto tracing) or if we cannot parse the response
-         * from the receiver. In either case, tracing is unlikely to work under these circumstances,
-         * and more context on how to proceed can be found in [exitCode] or [message] properties.
-         */
-        public val requiredVersion: String?,
-
-        public val message: String?
-    )
-}
diff --git a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/handshake/PerfettoSdkHandshake.kt b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/handshake/PerfettoSdkHandshake.kt
new file mode 100644
index 0000000..026828e
--- /dev/null
+++ b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/handshake/PerfettoSdkHandshake.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tracing.perfetto.handshake
+
+import androidx.tracing.perfetto.handshake.protocol.EnableTracingResponse
+import androidx.tracing.perfetto.handshake.protocol.RequestKeys.ACTION_ENABLE_TRACING
+import androidx.tracing.perfetto.handshake.protocol.RequestKeys.ACTION_ENABLE_TRACING_COLD_START
+import androidx.tracing.perfetto.handshake.protocol.RequestKeys.KEY_PATH
+import androidx.tracing.perfetto.handshake.protocol.RequestKeys.KEY_PERSISTENT
+import androidx.tracing.perfetto.handshake.protocol.RequestKeys.RECEIVER_CLASS_NAME
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes
+import androidx.tracing.perfetto.handshake.protocol.ResponseKeys.KEY_EXIT_CODE
+import androidx.tracing.perfetto.handshake.protocol.ResponseKeys.KEY_MESSAGE
+import androidx.tracing.perfetto.handshake.protocol.ResponseKeys.KEY_REQUIRED_VERSION
+import java.io.File
+
+/**
+ * Handshake implementation allowing to enable Perfetto SDK tracing in an app that enables it.
+ *
+ * @param targetPackage package name of the target app
+ * @param parseJsonMap function parsing a flat map in a JSON format into a `Map<String, String>`
+ * e.g. `"{ 'key 1': 'value 1', 'key 2': 'value 2' }"` ->
+ * `mapOf("key 1" to "value 1", "key 2" to "value 2")`
+ * @param executeShellCommand function allowing to execute `adb shell` commands on the target device
+ *
+ * For error handling, note that [parseJsonMap] and [executeShellCommand] will be called on the same
+ * thread as [enableTracingImmediate] and [enableTracingColdStart].
+ */
+public class PerfettoSdkHandshake(
+    private val targetPackage: String,
+    private val parseJsonMap: (jsonString: String) -> Map<String, String>,
+    private val executeShellCommand: ShellCommandExecutor
+) {
+    /**
+     * Attempts to enable tracing in an app. It will wake up (or start) the app process, so it will
+     * act as warm/hot tracing. For cold tracing see [enableTracingColdStart]
+     *
+     * Note: if the app process is not running, it will be launched making the method a bad choice
+     * for cold tracing (use [enableTracingColdStart] instead.
+     *
+     * @param librarySource optional AAR or an APK containing `libtracing_perfetto.so`
+     */
+    public fun enableTracingImmediate(
+        librarySource: LibrarySource? = null
+    ): EnableTracingResponse {
+        val libPath = librarySource?.run {
+            PerfettoSdkSideloader(targetPackage).sideloadFromZipFile(
+                libraryZip,
+                tempDirectory,
+                executeShellCommand,
+                moveLibFileFromTmpDirToAppDir
+            )
+        }
+        return sendEnableTracingBroadcast(libPath, coldStart = false)
+    }
+
+    /**
+     * Attempts to prepare cold startup tracing in an app.
+     *
+     * @param killAppProcess function responsible for terminating the app process (no-op if the
+     * process is already terminated)
+     * @param librarySource optional AAR or an APK containing `libtracing_perfetto.so`
+     */
+    public fun enableTracingColdStart(
+        killAppProcess: () -> Unit,
+        librarySource: LibrarySource?
+    ): EnableTracingResponse {
+        // sideload the `libtracing_perfetto.so` file if applicable
+        val libPath = librarySource?.run {
+            PerfettoSdkSideloader(targetPackage).sideloadFromZipFile(
+                libraryZip,
+                tempDirectory,
+                executeShellCommand,
+                moveLibFileFromTmpDirToAppDir
+            )
+        }
+
+        // ensure a clean start (e.g. in case tracing is already enabled)
+        killAppProcess()
+
+        // verify (by performing a regular handshake) that we can enable tracing at app startup
+        val response = sendEnableTracingBroadcast(libPath, coldStart = true, persistent = false)
+        if (response.exitCode == ResponseExitCodes.RESULT_CODE_SUCCESS) {
+            // terminate the app process (that we woke up by issuing a broadcast earlier)
+            killAppProcess()
+        }
+
+        return response
+    }
+
+    private fun sendEnableTracingBroadcast(
+        libPath: File? = null,
+        coldStart: Boolean,
+        persistent: Boolean? = null
+    ): EnableTracingResponse {
+        val action = if (coldStart) ACTION_ENABLE_TRACING_COLD_START else ACTION_ENABLE_TRACING
+        val commandBuilder = StringBuilder("am broadcast -a $action")
+        if (persistent != null) commandBuilder.append(" --es $KEY_PERSISTENT $persistent")
+        if (libPath != null) commandBuilder.append(" --es $KEY_PATH $libPath")
+        commandBuilder.append(" $targetPackage/$RECEIVER_CLASS_NAME")
+
+        val rawResponse = executeShellCommand(commandBuilder.toString())
+
+        val response = try {
+            parseResponse(rawResponse)
+        } catch (e: IllegalArgumentException) {
+            val message = "Exception occurred while trying to parse a response." +
+                " Error: ${e.message}. Raw response: $rawResponse."
+            EnableTracingResponse(ResponseExitCodes.RESULT_CODE_ERROR_OTHER, null, message)
+        }
+        return response
+    }
+
+    private fun parseResponse(rawResponse: String): EnableTracingResponse {
+        val line = rawResponse
+            .split(Regex("\r?\n"))
+            .firstOrNull { it.contains("Broadcast completed: result=") }
+            ?: throw IllegalArgumentException("Cannot parse: $rawResponse")
+
+        if (line == "Broadcast completed: result=0") return EnableTracingResponse(
+            ResponseExitCodes.RESULT_CODE_CANCELLED, null, null
+        )
+
+        val matchResult =
+            Regex("Broadcast completed: (result=.*?)(, data=\".*?\")?(, extras: .*)?")
+                .matchEntire(line)
+                ?: throw IllegalArgumentException("Cannot parse: $rawResponse")
+
+        val broadcastResponseCode = matchResult
+            .groups[1]
+            ?.value
+            ?.substringAfter("result=")
+            ?.toIntOrNull()
+
+        val dataString = matchResult
+            .groups
+            .firstOrNull { it?.value?.startsWith(", data=") ?: false }
+            ?.value
+            ?.substringAfter(", data=\"")
+            ?.dropLast(1)
+            ?: throw IllegalArgumentException("Cannot parse: $rawResponse. " +
+                "Unable to detect 'data=' section."
+            )
+
+        val dataMap = parseJsonMap(dataString)
+        val response = EnableTracingResponse(
+            dataMap[KEY_EXIT_CODE]?.toInt()
+                ?: throw IllegalArgumentException("Response missing $KEY_EXIT_CODE value"),
+            dataMap[KEY_REQUIRED_VERSION]
+                ?: throw IllegalArgumentException("Response missing $KEY_REQUIRED_VERSION value"),
+            dataMap[KEY_MESSAGE]
+        )
+
+        if (broadcastResponseCode != response.exitCode) {
+            throw IllegalStateException(
+                "Cannot parse: $rawResponse. Exit code " +
+                    "not matching broadcast exit code."
+            )
+        }
+
+        return response
+    }
+
+    /**
+    * @param libraryZip either an AAR or an APK containing `libtracing_perfetto.so`
+    * @param tempDirectory a directory directly accessible to the caller process (used for
+     * extraction of the binaries from the zip)
+    * @param moveLibFileFromTmpDirToAppDir a function capable of moving the binary file from
+    * the [tempDirectory] to an app accessible folder
+    */
+    // TODO(245426369): consider moving to a factory pattern for constructing these and refer to
+    //  this one as `aarLibrarySource` and `apkLibrarySource`
+    public class LibrarySource @Suppress("StreamFiles") constructor(
+        internal val libraryZip: File,
+        internal val tempDirectory: File,
+        internal val moveLibFileFromTmpDirToAppDir: FileMover
+    )
+}
diff --git a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoSdkSideloader.kt b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/handshake/PerfettoSdkSideloader.kt
similarity index 98%
rename from tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoSdkSideloader.kt
rename to tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/handshake/PerfettoSdkSideloader.kt
index 9dfb51a..df50de5 100644
--- a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoSdkSideloader.kt
+++ b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/handshake/PerfettoSdkSideloader.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.tracing.perfetto
+package androidx.tracing.perfetto.handshake
 
 import java.io.File
 import java.util.zip.ZipFile
diff --git a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/handshake/protocol/Protocol.kt b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/handshake/protocol/Protocol.kt
new file mode 100644
index 0000000..c23dfc1
--- /dev/null
+++ b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/handshake/protocol/Protocol.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tracing.perfetto.handshake.protocol
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+
+@RestrictTo(LIBRARY_GROUP)
+public object RequestKeys {
+    public const val RECEIVER_CLASS_NAME: String = "androidx.tracing.perfetto.TracingReceiver"
+
+    /**
+     * Request to enable tracing in an app.
+     *
+     * The action is performed straight away allowing for warm / hot tracing. For cold start
+     * tracing see [ACTION_ENABLE_TRACING_COLD_START]
+     *
+     * Request can include [KEY_PATH] as an optional extra.
+     *
+     * Response to the request is a JSON string (to allow for CLI support) with the following:
+     * - [ResponseKeys.KEY_EXIT_CODE] (always)
+     * - [ResponseKeys.KEY_REQUIRED_VERSION] (always)
+     * - [ResponseKeys.KEY_MESSAGE] (optional)
+     */
+    public const val ACTION_ENABLE_TRACING: String =
+        "androidx.tracing.perfetto.action.ENABLE_TRACING"
+
+    /**
+     * Request to enable cold start tracing in an app.
+     *
+     * For warm / hot tracing, see [ACTION_ENABLE_TRACING].
+     *
+     * The action must be performed in the following order, otherwise its effects are
+     * unspecified:
+     * - the app process must be killed before performing the action
+     * - the action must then follow
+     * - the app process must be killed after performing the action
+     *
+     * Request can include [KEY_PATH] as an optional extra.
+     * Request can include [KEY_PERSISTENT] as an optional extra.
+     *
+     * Response to the request is a JSON string (to allow for CLI support) with the following:
+     * - [ResponseKeys.KEY_EXIT_CODE] (always)
+     * - [ResponseKeys.KEY_REQUIRED_VERSION] (always)
+     * - [ResponseKeys.KEY_MESSAGE] (optional)
+     */
+    public const val ACTION_ENABLE_TRACING_COLD_START: String =
+        "androidx.tracing.perfetto.action.ENABLE_TRACING_COLD_START"
+
+    /**
+     * Request to disable cold start tracing (previously enabled with
+     * [ACTION_ENABLE_TRACING_COLD_START]).
+     *
+     * The action is particularly useful when cold start tracing was enabled in
+     * [KEY_PERSISTENT] mode.
+     *
+     * The action must be performed in the following order, otherwise its effects are
+     * unspecified:
+     * - the app process must be killed before performing the action
+     * - the action must then follow
+     * - the app process must be killed after performing the action
+     *
+     * Request can include [KEY_PATH] as an optional extra.
+     * Request can include [KEY_PERSISTENT] as an optional extra.
+     *
+     * Response to the request is a JSON string (to allow for CLI support) with the following:
+     * - [ResponseKeys.KEY_EXIT_CODE] (always)
+     */
+    public const val ACTION_DISABLE_TRACING_COLD_START: String =
+        "androidx.tracing.perfetto.action.DISABLE_TRACING_COLD_START"
+
+    /** Path to tracing native binary file */
+    public const val KEY_PATH: String = "path"
+
+    /**
+     * Boolean flag to signify whether the operation should be persistent between runs
+     * (or only performed once).
+     *
+     * Applies to [ACTION_ENABLE_TRACING_COLD_START]
+     */
+    public const val KEY_PERSISTENT: String = "persistent"
+}
+
+@RestrictTo(LIBRARY_GROUP)
+public object ResponseKeys {
+    /** Exit code as listed in [ResponseExitCodes]. */
+    public const val KEY_EXIT_CODE: String = "exitCode"
+
+    /**
+     * Required version of the binaries. Java and binary library versions have to match to
+     * ensure compatibility. In the Maven format, e.g. 1.2.3-beta01.
+     */
+    public const val KEY_REQUIRED_VERSION: String = "requiredVersion"
+
+    /**
+     * Message string that gives more information about the response, e.g. recovery steps
+     * if applicable.
+     */
+    public const val KEY_MESSAGE: String = "message"
+}
+
+public object ResponseExitCodes {
+    /**
+     * Indicates that the broadcast resulted in `result=0`, which is an equivalent
+     * of [android.app.Activity.RESULT_CANCELED].
+     *
+     * This most likely means that the app does not expose a [PerfettoSdkHandshake] compatible
+     * receiver.
+     */
+    @Suppress("KDocUnresolvedReference")
+    public const val RESULT_CODE_CANCELLED: Int = 0
+
+    public const val RESULT_CODE_SUCCESS: Int = 1
+    public const val RESULT_CODE_ALREADY_ENABLED: Int = 2
+
+    /**
+     * Required version described in [EnableTracingResponse.requiredVersion].
+     * A follow-up [androidx.tracing.perfetto.handshake.PerfettoSdkHandshake.enableTracingImmediate]
+     * request expected with binaries to sideload specified.
+     */
+    public const val RESULT_CODE_ERROR_BINARY_MISSING: Int = 11
+
+    /** Required version described in [EnableTracingResponse.requiredVersion]. */
+    public const val RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH: Int = 12
+
+    /**
+     * Could be a result of a stale version of the binary cached locally.
+     * Retrying with a freshly downloaded library likely to fix the issue.
+     * More specific information in [EnableTracingResponse.message]
+     */
+    public const val RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR: Int = 13
+
+    /** More specific information in [EnableTracingResponse.message] */
+    public const val RESULT_CODE_ERROR_OTHER: Int = 99
+}
+
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(
+    ResponseExitCodes.RESULT_CODE_CANCELLED,
+    ResponseExitCodes.RESULT_CODE_SUCCESS,
+    ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED,
+    ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING,
+    ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH,
+    ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR,
+    ResponseExitCodes.RESULT_CODE_ERROR_OTHER
+)
+private annotation class EnableTracingResultCode
+
+public class EnableTracingResponse @RestrictTo(LIBRARY_GROUP) constructor(
+    @EnableTracingResultCode public val exitCode: Int,
+
+    /**
+     * This can be `null` iff we cannot communicate with the broadcast receiver of the target
+     * process (e.g. app does not offer Perfetto tracing) or if we cannot parse the response
+     * from the receiver. In either case, tracing is unlikely to work under these circumstances,
+     * and more context on how to proceed can be found in [exitCode] or [message] properties.
+     */
+    public val requiredVersion: String?,
+
+    public val message: String?
+)
diff --git a/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/test/TracingTest.kt b/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/test/TracingTest.kt
index 233ee75..6fa6336 100644
--- a/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/test/TracingTest.kt
+++ b/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/test/TracingTest.kt
@@ -20,9 +20,9 @@
 import androidx.annotation.RequiresApi
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import androidx.tracing.perfetto.PerfettoSdkHandshake.RequestKeys.RECEIVER_CLASS_NAME
 import androidx.tracing.perfetto.Tracing
 import androidx.tracing.perfetto.TracingReceiver
+import androidx.tracing.perfetto.handshake.protocol.RequestKeys.RECEIVER_CLASS_NAME
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/Tracing.kt b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/Tracing.kt
index a97d57f..bd73a63 100644
--- a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/Tracing.kt
+++ b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/Tracing.kt
@@ -18,13 +18,13 @@
 import android.content.Context
 import android.os.Build
 import androidx.annotation.RequiresApi
-import androidx.tracing.perfetto.PerfettoSdkHandshake.EnableTracingResponse
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ERROR_OTHER
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_SUCCESS
+import androidx.tracing.perfetto.handshake.protocol.EnableTracingResponse
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ERROR_OTHER
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_SUCCESS
 import androidx.tracing.perfetto.jni.PerfettoNative
 import androidx.tracing.perfetto.security.IncorrectChecksumException
 import androidx.tracing.perfetto.security.SafeLibLoader
diff --git a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/TracingReceiver.kt b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/TracingReceiver.kt
index 43fd4e3..949df8e 100644
--- a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/TracingReceiver.kt
+++ b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/TracingReceiver.kt
@@ -23,16 +23,16 @@
 import android.util.JsonWriter
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope.LIBRARY
-import androidx.tracing.perfetto.PerfettoSdkHandshake.EnableTracingResponse
-import androidx.tracing.perfetto.PerfettoSdkHandshake.RequestKeys.ACTION_ENABLE_TRACING
-import androidx.tracing.perfetto.PerfettoSdkHandshake.RequestKeys.ACTION_ENABLE_TRACING_COLD_START
-import androidx.tracing.perfetto.PerfettoSdkHandshake.RequestKeys.KEY_PATH
-import androidx.tracing.perfetto.PerfettoSdkHandshake.RequestKeys.KEY_PERSISTENT
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_ERROR_OTHER
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseExitCodes.RESULT_CODE_SUCCESS
-import androidx.tracing.perfetto.PerfettoSdkHandshake.ResponseKeys
 import androidx.tracing.perfetto.StartupTracingConfigStore.store
 import androidx.tracing.perfetto.Tracing.EnableTracingResponse
+import androidx.tracing.perfetto.handshake.protocol.EnableTracingResponse
+import androidx.tracing.perfetto.handshake.protocol.RequestKeys.ACTION_ENABLE_TRACING
+import androidx.tracing.perfetto.handshake.protocol.RequestKeys.ACTION_ENABLE_TRACING_COLD_START
+import androidx.tracing.perfetto.handshake.protocol.RequestKeys.KEY_PATH
+import androidx.tracing.perfetto.handshake.protocol.RequestKeys.KEY_PERSISTENT
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_ERROR_OTHER
+import androidx.tracing.perfetto.handshake.protocol.ResponseExitCodes.RESULT_CODE_SUCCESS
+import androidx.tracing.perfetto.handshake.protocol.ResponseKeys
 import java.io.File
 import java.io.StringWriter
 import java.util.concurrent.LinkedBlockingQueue
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index 555b6b2..4e3d505 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -16,19 +16,30 @@
 
 package androidx.wear.compose.material3.demos
 
+import androidx.compose.ui.Alignment
+import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.integration.demos.common.Centralize
 import androidx.wear.compose.integration.demos.common.ComposableDemo
 import androidx.wear.compose.integration.demos.common.DemoCategory
+import androidx.wear.compose.material3.samples.AppCardSample
+import androidx.wear.compose.material3.samples.AppCardWithIconSample
+import androidx.wear.compose.material3.samples.CardSample
 import androidx.wear.compose.material3.samples.FixedFontSize
+import androidx.wear.compose.material3.samples.OutlinedAppCardSample
+import androidx.wear.compose.material3.samples.OutlinedCardSample
+import androidx.wear.compose.material3.samples.OutlinedTitleCardSample
 import androidx.wear.compose.material3.samples.StepperSample
 import androidx.wear.compose.material3.samples.StepperWithIntegerSample
 import androidx.wear.compose.material3.samples.StepperWithRangeSemanticsSample
+import androidx.wear.compose.material3.samples.TitleCardSample
+import androidx.wear.compose.material3.samples.TitleCardWithImageSample
 
 val WearMaterial3Demos = DemoCategory(
     "Material 3",
     listOf(
         DemoCategory(
-            "Buttons",
+            "Button",
             listOf(
                 ComposableDemo("Button") {
                     ButtonDemo()
@@ -44,6 +55,26 @@
                 }
             )
         ),
+        DemoCategory(
+            "Card",
+            listOf(
+                ComposableDemo("Samples") {
+                    ScalingLazyColumn(
+                        horizontalAlignment = Alignment.CenterHorizontally,
+                        autoCentering = AutoCenteringParams(itemIndex = 0)
+                    ) {
+                        item { CardSample() }
+                        item { AppCardSample() }
+                        item { AppCardWithIconSample() }
+                        item { TitleCardSample() }
+                        item { TitleCardWithImageSample() }
+                        item { OutlinedCardSample() }
+                        item { OutlinedAppCardSample() }
+                        item { OutlinedTitleCardSample() }
+                    }
+                }
+            )
+        ),
         ComposableDemo("Text Button") {
             TextButtonDemo()
         },
@@ -66,12 +97,6 @@
                             Centralize { StepperWithRangeSemanticsSample() }
                         }
                     )
-                ),
-                DemoCategory(
-                    "Demos",
-                    listOf(
-                        // Add Stepper demos here
-                    )
                 )
             )
         ),
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/CardSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/CardSample.kt
new file mode 100644
index 0000000..4e5af0f
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/CardSample.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.wear.compose.material3.AppCard
+import androidx.wear.compose.material3.Card
+import androidx.wear.compose.material3.CardDefaults
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.OutlinedCard
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TitleCard
+
+@Sampled
+@Composable
+fun CardSample() {
+    Card(
+        onClick = { /* Do something */ },
+    ) {
+        Text("Card")
+    }
+}
+
+@Sampled
+@Composable
+fun AppCardSample() {
+    AppCard(
+        onClick = { /* Do something */ },
+        appName = { Text("AppName") },
+        title = { Text("AppCard") },
+        time = { Text("now") },
+    ) {
+        Text("Card content")
+    }
+}
+
+@Sampled
+@Composable
+fun AppCardWithIconSample() {
+    AppCard(
+        onClick = { /* Do something */ },
+        appName = { Text("AppName") },
+        appImage = {
+            Icon(
+                painter = painterResource(id = android.R.drawable.star_big_off),
+                contentDescription = "favourites",
+                modifier = Modifier
+                    .size(CardDefaults.AppImageSize)
+                    .wrapContentSize(align = Alignment.Center),
+            )
+        },
+        title = { Text("AppCard with icon") },
+        time = { Text("now") },
+    ) {
+        Text("Card content")
+    }
+}
+
+@Sampled
+@Composable
+fun TitleCardSample() {
+    TitleCard(
+        onClick = { /* Do something */ },
+        title = { Text("TitleCard") },
+        time = { Text("now") },
+    ) {
+        Text("Card content")
+    }
+}
+
+@Sampled
+@Composable
+fun TitleCardWithImageSample() {
+    TitleCard(
+        onClick = { /* Do something */ },
+        title = { Text("TitleCard With an ImageBackground") },
+        colors = CardDefaults.imageCardColors(
+            containerPainter = CardDefaults.imageWithScrimBackgroundPainter(
+                backgroundImagePainter = painterResource(id = R.drawable.backgroundimage)
+            ),
+            contentColor = MaterialTheme.colorScheme.onSurface,
+            titleColor = MaterialTheme.colorScheme.onSurface
+        )
+    ) {
+        Text("Card content")
+    }
+}
+
+@Sampled
+@Composable
+fun OutlinedCardSample() {
+    OutlinedCard(
+        onClick = { /* Do something */ },
+    ) {
+        Text("OutlinedCard")
+    }
+}
+
+@Sampled
+@Composable
+fun OutlinedAppCardSample() {
+    AppCard(
+        onClick = { /* Do something */ },
+        appName = { Text("AppName") },
+        title = { Text("Outlined AppCard") },
+        colors = CardDefaults.outlinedCardColors(),
+        border = CardDefaults.outlinedCardBorder(),
+    ) {
+        Text("Card content")
+    }
+}
+
+@Sampled
+@Composable
+fun OutlinedTitleCardSample() {
+    TitleCard(
+        onClick = { /* Do something */ },
+        title = { Text("Outlined TitleCard") },
+        colors = CardDefaults.outlinedCardColors(),
+        border = CardDefaults.outlinedCardBorder(),
+    ) {
+        Text("Card content")
+    }
+}
diff --git a/wear/compose/compose-material3/samples/src/main/res/drawable/backgroundimage.png b/wear/compose/compose-material3/samples/src/main/res/drawable/backgroundimage.png
new file mode 100644
index 0000000..ea5a397
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/res/drawable/backgroundimage.png
Binary files differ
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt
index 5454a04..8ae62c8 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt
@@ -50,6 +50,9 @@
  *
  * Cards can be enabled or disabled. A disabled card will not respond to click events.
  *
+ * Example of a [Card]:
+ * @sample androidx.wear.compose.material3.samples.CardSample
+ *
  * For more information, see the
  * [Cards](https://developer.android.com/training/wearables/components/cards)
  * Wear OS Material design guide.
@@ -127,6 +130,15 @@
  * If more than one composable is provided in the content slot it is the responsibility of the
  * caller to determine how to layout the contents, e.g. provide either a row or a column.
  *
+ * Example of an [AppCard]:
+ * @sample androidx.wear.compose.material3.samples.AppCardSample
+ *
+ * Example of an [AppCard] with icon:
+ * @sample androidx.wear.compose.material3.samples.AppCardWithIconSample
+ *
+ * Example of an outlined [AppCard]:
+ * @sample androidx.wear.compose.material3.samples.OutlinedAppCardSample
+ *
  * For more information, see the
  * [Cards](https://developer.android.com/training/wearables/components/cards)
  * guide.
@@ -242,6 +254,15 @@
  * If more than one composable is provided in the content slot it is the responsibility of the
  * caller to determine how to layout the contents, e.g. provide either a row or a column.
  *
+ * Example of a [TitleCard]:
+ * @sample androidx.wear.compose.material3.samples.TitleCardSample
+ *
+ * Example of a [TitleCard] with image:
+ * @sample androidx.wear.compose.material3.samples.TitleCardWithImageSample
+ *
+ * Example of an outlined [TitleCard]:
+ * @sample androidx.wear.compose.material3.samples.OutlinedTitleCardSample
+ *
  * For more information, see the
  * [Cards](https://developer.android.com/training/wearables/components/cards)
  * guide.
@@ -333,6 +354,9 @@
  *
  * Cards can be enabled or disabled. A disabled card will not respond to click events.
  *
+ * Example of an [OutlinedCard]:
+ * @sample androidx.wear.compose.material3.samples.OutlinedCardSample
+ *
  * For more information, see the
  * [Cards](https://developer.android.com/training/wearables/components/cards)
  * Wear OS Material design guide.
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 66e0a8b..fa0c223 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
         applicationId "androidx.wear.compose.integration.demos"
         minSdk 25
         targetSdk 30
-        versionCode 14
-        versionName "1.14"
+        versionCode 15
+        versionName "1.15"
         // Change the APK name to match the *testapp regex we use to pick up APKs for testing as
         // part of CI.
         archivesBaseName = "wear-compose-demos-testapp"