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"