Merge "Add a new View class for the loading screen." into androidx-main
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt
index 4db6b75..08b4165 100644
--- a/browser/browser/api/current.txt
+++ b/browser/browser/api/current.txt
@@ -303,6 +303,7 @@
method public boolean mayLaunchUrl(android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
method @androidx.browser.customtabs.CustomTabsService.Result public int postMessage(String, android.os.Bundle?);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPrefetch public void prefetch(android.net.Uri, androidx.browser.customtabs.PrefetchOptions);
+ method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPrefetch public void prefetch(java.util.List<android.net.Uri!>, androidx.browser.customtabs.PrefetchOptions);
method public boolean receiveFile(android.net.Uri, int, android.os.Bundle?);
method public boolean requestPostMessageChannel(android.net.Uri);
method public boolean requestPostMessageChannel(android.net.Uri, android.net.Uri?, android.os.Bundle);
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt
index 82f71f0..8df6fe2 100644
--- a/browser/browser/api/restricted_current.txt
+++ b/browser/browser/api/restricted_current.txt
@@ -314,6 +314,7 @@
method public boolean mayLaunchUrl(android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
method @androidx.browser.customtabs.CustomTabsService.Result public int postMessage(String, android.os.Bundle?);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPrefetch public void prefetch(android.net.Uri, androidx.browser.customtabs.PrefetchOptions);
+ method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPrefetch public void prefetch(java.util.List<android.net.Uri!>, androidx.browser.customtabs.PrefetchOptions);
method public boolean receiveFile(android.net.Uri, int, android.os.Bundle?);
method public boolean requestPostMessageChannel(android.net.Uri);
method public boolean requestPostMessageChannel(android.net.Uri, android.net.Uri?, android.os.Bundle);
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
index e7a87c7..ca297bb 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
@@ -136,6 +136,26 @@
}
/**
+ * Request the browser to start navigational prefetch to the pages that will be used for future
+ * navigations.
+ *
+ * @param urls The urls to be prefetched for upcoming navigations.
+ * @param options The option used for prefetch request. Please see
+ * {@link PrefetchOptions}.
+ */
+ @ExperimentalPrefetch
+ @SuppressWarnings("NullAway") // TODO: b/142938599
+ public void prefetch(@NonNull List<Uri> urls, @NonNull PrefetchOptions options) {
+ try {
+ for (Uri uri : urls) {
+ mService.prefetch(mCallback, uri, options.toBundle());
+ }
+ } catch (RemoteException e) {
+ return;
+ }
+ }
+
+ /**
* This sets the action button on the toolbar with ID
* {@link CustomTabsIntent#TOOLBAR_ACTION_BUTTON_ID}.
*
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index e36662f..93fa12e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -184,11 +184,18 @@
}
// Add Compose compiler plugin to kotlinPlugin configuration, making sure it works
// for Playground builds as well
- val pluginVersionToml = project.getVersionByName("composeCompilerPlugin")
- val versionToUse =
- if (ProjectLayoutType.isPlayground(project)) {
- pluginVersionToml
- } else {
+ val compilerPluginVersion = project.getVersionByName("composeCompilerPlugin")
+ project.dependencies.add(
+ COMPILER_PLUGIN_CONFIGURATION,
+ "androidx.compose.compiler:compiler:$compilerPluginVersion"
+ )
+
+ if (
+ !ProjectLayoutType.isPlayground(project) &&
+ // ksp is also a compiler plugin, updating Kotlin for it will likely break the build
+ !project.plugins.hasPlugin("com.google.devtools.ksp")
+ ) {
+ if (compilerPluginVersion.endsWith("-SNAPSHOT")) {
// use exact project path instead of subprojects.find, it is faster
val compilerProject = project.rootProject.resolveProject(":compose")
val compilerMavenDirectory =
@@ -196,22 +203,30 @@
compilerProject.projectDir,
"compiler/compose-compiler-snapshot-repository"
)
- if (!compilerMavenDirectory.exists()) {
- pluginVersionToml
- } else {
- project.repositories.maven { it.url = compilerMavenDirectory.toURI() }
- // Version chosen to be not a "-SNAPSHOT" since apparently gradle doesn't
- // validate signatures for -SNAPSHOT builds. Version is chosen to be higher
- // than anything real to ensure it is seen as newer than any explicit dependency
- // to prevent gradle from "upgrading" to a stable build instead of local build.
- // This version is built by: snapshot-compose-compiler.sh (in compiler project)
- "99.0.0"
+ project.repositories.maven { it.url = compilerMavenDirectory.toURI() }
+ project.configurations.configureEach {
+ it.resolutionStrategy.eachDependency { dep ->
+ val requested = dep.requested
+ if (
+ requested.group == "org.jetbrains.kotlin" &&
+ (requested.name == "kotlin-compiler-embeddable" ||
+ requested.name == "kotlin-compose-compiler-plugin-embeddable")
+ ) {
+ dep.useVersion(compilerPluginVersion)
+ }
+
+ if (
+ requested.group == "androidx.compose.compiler" &&
+ requested.name == "compiler"
+ ) {
+ dep.useTarget(
+ "org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:$compilerPluginVersion"
+ )
+ }
+ }
}
}
- project.dependencies.add(
- COMPILER_PLUGIN_CONFIGURATION,
- "androidx.compose.compiler:compiler:$versionToUse"
- )
+ }
val kotlinPluginProvider =
project.provider {
@@ -238,7 +253,7 @@
compile.pluginClasspath.from(kotlinPluginProvider.get())
- // todo(b/291587160): enable when Compose compiler 2.0 is merged
+ // todo(b/291587160): enable when Compose compiler 2.0.20 is merged
// compile.enableFeatureFlag(ComposeFeatureFlag.StrongSkipping)
// compile.enableFeatureFlag(ComposeFeatureFlag.OptimizeNonSkippingGroups)
compile.addPluginOption(ComposeCompileOptions.StrongSkipping, "true")
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/ApkCopyHelper.kt b/buildSrc/public/src/main/kotlin/androidx/build/ApkCopyHelper.kt
index 1f3e69a..f139a1b 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/ApkCopyHelper.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/ApkCopyHelper.kt
@@ -61,7 +61,7 @@
project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java)?.apply {
onVariants(selector().withBuildType(buildType)) { variant ->
val apkCopy =
- project.tasks.register("copyAppApk", ApkCopyTask::class.java) { task ->
+ project.tasks.register("copyAppApk-$buildType", ApkCopyTask::class.java) { task ->
task.apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
task.apkLoader.set(variant.artifacts.getBuiltArtifactsLoader())
val file =
diff --git a/busytown/androidx_multiplatform_mac_host_tests_arm64.sh b/busytown/androidx_multiplatform_mac_host_tests_arm64.sh
index 5319a5c..8f9c2af 100755
--- a/busytown/androidx_multiplatform_mac_host_tests_arm64.sh
+++ b/busytown/androidx_multiplatform_mac_host_tests_arm64.sh
@@ -15,7 +15,7 @@
# Setup simulators
impl/androidx-native-mac-simulator-setup.sh
-impl/build.sh darwinBenchmarkResults allHostTests \
+impl/build.sh allHostTests \
--no-configuration-cache \
-Pandroidx.ignoreTestFailures \
-Pandroidx.displayTestOutput=false \
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
index 89c6a9f..1e453f4 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
@@ -566,7 +566,7 @@
addSurface(deferrableSurface)
}
- updateSessionConfig(sessionConfigBuilder.build())
+ updateSessionConfig(listOf(sessionConfigBuilder.build()))
notifyActive()
}
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
index 4bbe5a4..d36f218 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
@@ -205,7 +205,7 @@
) : FakeUseCase(config) {
fun setupSessionConfig(sessionConfigBuilder: SessionConfig.Builder) {
- updateSessionConfig(sessionConfigBuilder.build())
+ updateSessionConfig(listOf(sessionConfigBuilder.build()))
notifyActive()
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
index 28d9ce3..5810c6e 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
@@ -264,7 +264,7 @@
private fun createFakeUseCase() =
object : FakeUseCase(FakeUseCaseConfig.Builder().setTargetName("UseCase").useCaseConfig) {
fun setupSessionConfig(sessionConfig: SessionConfig) {
- updateSessionConfig(sessionConfig)
+ updateSessionConfig(listOf(sessionConfig))
notifyActive()
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
index 9c2c3dd..b5474c5 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -133,6 +133,7 @@
* @param newUseCaseConfigsSupportedSizeMap map of configurations of the use cases to the
* supported sizes list that will be given a suggested stream specification
* @param isPreviewStabilizationOn whether the preview stabilization is enabled.
+ * @param hasVideoCapture whether the use cases has video capture.
* @return map of suggested stream specifications for given use cases
* @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if there
* isn't a supported combination of surfaces available, or if the {@code cameraId} is not a
@@ -143,7 +144,8 @@
cameraId: String,
existingSurfaces: List<AttachedSurfaceInfo>,
newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>,
- isPreviewStabilizationOn: Boolean
+ isPreviewStabilizationOn: Boolean,
+ hasVideoCapture: Boolean
): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
if (!checkIfSupportedCombinationExist(cameraId)) {
@@ -156,7 +158,8 @@
cameraMode,
existingSurfaces,
newUseCaseConfigsSupportedSizeMap,
- isPreviewStabilizationOn
+ isPreviewStabilizationOn,
+ hasVideoCapture
)
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index b2441af..74bba1f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -235,6 +235,7 @@
* @param newUseCaseConfigsSupportedSizeMap newly added UseCaseConfig to supported output sizes
* map.
* @param isPreviewStabilizationOn whether the preview stabilization is enabled.
+ * @param hasVideoCapture whether the use cases has video capture.
* @return the suggested stream specs, which is a mapping from UseCaseConfig to the suggested
* stream specification.
* @throws IllegalArgumentException if the suggested solution for newUseCaseConfigs cannot be
@@ -244,7 +245,8 @@
cameraMode: Int,
attachedSurfaces: List<AttachedSurfaceInfo>,
newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>,
- isPreviewStabilizationOn: Boolean = false
+ isPreviewStabilizationOn: Boolean = false,
+ hasVideoCapture: Boolean = false
): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
// Refresh Preview Size based on current display configurations.
refreshPreviewSize()
@@ -336,6 +338,7 @@
newUseCaseConfigs,
useCasesPriorityOrder,
resolvedDynamicRanges,
+ hasVideoCapture
)
val attachedSurfaceStreamSpecMap = mutableMapOf<AttachedSurfaceInfo, StreamSpec>()
@@ -808,6 +811,7 @@
newUseCaseConfigs: List<UseCaseConfig<*>>,
useCasesPriorityOrder: List<Int>,
resolvedDynamicRanges: Map<UseCaseConfig<*>, DynamicRange>,
+ hasVideoCapture: Boolean
): MutableMap<UseCaseConfig<*>, StreamSpec> {
val suggestedStreamSpecMap = mutableMapOf<UseCaseConfig<*>, StreamSpec>()
var targetFrameRateForDevice: Range<Int>? = null
@@ -824,6 +828,7 @@
.setImplementationOptions(
StreamUseCaseUtil.getStreamSpecImplementationOptions(useCaseConfig)
)
+ .setZslDisabled(hasVideoCapture)
if (targetFrameRateForDevice != null) {
streamSpecBuilder.setExpectedFrameRateRange(targetFrameRateForDevice)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 4887fdf..5dd8c6b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -74,10 +74,13 @@
override fun getUseCaseConfigBuilder(config: Config) =
Builder(cameraProperties, displayInfoManager)
- override fun onSuggestedStreamSpecUpdated(suggestedStreamSpec: StreamSpec): StreamSpec {
- updateSessionConfig(createPipeline(meteringSurfaceSize).build())
+ override fun onSuggestedStreamSpecUpdated(
+ primaryStreamSpec: StreamSpec,
+ secondaryStreamSpec: StreamSpec?,
+ ): StreamSpec {
+ updateSessionConfig(listOf(createPipeline(meteringSurfaceSize).build()))
notifyActive()
- return suggestedStreamSpec.toBuilder().setResolution(meteringSurfaceSize).build()
+ return primaryStreamSpec.toBuilder().setResolution(meteringSurfaceSize).build()
}
override fun onUnbind() {
@@ -91,7 +94,7 @@
fun setupSession() {
// The suggested stream spec passed to `updateSuggestedStreamSpec` doesn't matter since
// this use case uses the min preview size.
- updateSuggestedStreamSpec(StreamSpec.builder(DEFAULT_PREVIEW_SIZE).build())
+ updateSuggestedStreamSpec(StreamSpec.builder(DEFAULT_PREVIEW_SIZE).build(), null)
}
private fun createPipeline(resolution: Size): SessionConfig.Builder {
@@ -118,7 +121,7 @@
// Closes the old error listener if there is
closeableErrorListener?.close()
val errorListener = CloseableErrorListener { _, _ ->
- updateSessionConfig(createPipeline(resolution).build())
+ updateSessionConfig(listOf(createPipeline(resolution).build()))
notifyReset()
}
closeableErrorListener = errorListener
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 1efebfd..aaefadf 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -549,7 +549,7 @@
@GuardedBy("lock")
private fun addRepeatingUseCase() {
- meteringRepeating.bindToCamera(cameraInternal.get(), null, null)
+ meteringRepeating.bindToCamera(cameraInternal.get(), null, null, null)
meteringRepeating.setupSession()
attach(listOf(meteringRepeating))
activate(meteringRepeating)
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 dc0d559..b5bcfe9 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
@@ -637,7 +637,7 @@
fun customFovAdjusted() {
// 16:9 to 4:3
val useCase = FakeUseCase()
- useCase.updateSuggestedStreamSpec(StreamSpec.builder(Size(1920, 1080)).build())
+ useCase.updateSuggestedStreamSpec(StreamSpec.builder(Size(1920, 1080)).build(), null)
val factory = SurfaceOrientedMeteringPointFactory(1.0f, 1.0f, useCase)
val point = factory.createPoint(0f, 0f)
@@ -1723,9 +1723,10 @@
)
}
.also {
- it.bindToCamera(FakeCamera("0"), null, null)
+ it.bindToCamera(FakeCamera("0"), null, null, null)
it.updateSuggestedStreamSpec(
- StreamSpec.builder(suggestedStreamSpecResolution).build()
+ StreamSpec.builder(suggestedStreamSpecResolution).build(),
+ null
)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SessionConfigAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SessionConfigAdapterTest.kt
index 69d37a9..a11268a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SessionConfigAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SessionConfigAdapterTest.kt
@@ -150,7 +150,7 @@
var cameraControlReady = false
fun setupSessionConfig(sessionConfigBuilder: SessionConfig.Builder) {
- updateSessionConfig(sessionConfigBuilder.build())
+ updateSessionConfig(listOf(sessionConfigBuilder.build()))
notifyActive()
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 52a48a5..551b056b 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -24,6 +24,7 @@
import android.hardware.camera2.CameraCharacteristics.REQUEST_RECOMMENDED_TEN_BIT_DYNAMIC_RANGE_PROFILE
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
import android.hardware.camera2.params.DynamicRangeProfiles
import android.hardware.camera2.params.StreamConfigurationMap
import android.media.CamcorderProfile.QUALITY_1080P
@@ -79,6 +80,7 @@
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.UseCaseConfig
import androidx.camera.core.impl.UseCaseConfigFactory
+import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
import androidx.camera.core.internal.utils.SizeUtil
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1440P
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_720P
@@ -1466,6 +1468,34 @@
)
}
+ @Test
+ fun hasVideoCapture_suggestedStreamSpecZslDisabled() {
+ val useCase1 = createUseCase(CaptureType.VIDEO_CAPTURE) // VIDEO
+ val useCase2 = createUseCase(CaptureType.PREVIEW) // PREVIEW
+ val useCaseExpectedResultMap =
+ mutableMapOf<UseCase, Size>().apply {
+ put(useCase1, recordSize)
+ put(useCase2, previewSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ hasVideoCapture = true
+ )
+ }
+
+ @Test
+ fun hasNoVideoCapture_suggestedStreamSpecZslNotDisabled() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PREVIEW
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap =
+ mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, if (Build.VERSION.SDK_INT == 21) RESOLUTION_VGA else previewSize)
+ put(jpegUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap, hasVideoCapture = false)
+ }
+
private fun getSuggestedSpecsAndVerify(
useCasesExpectedResultMap: Map<UseCase, Size>,
attachedSurfaceInfoList: List<AttachedSurfaceInfo> = emptyList(),
@@ -1477,6 +1507,8 @@
useCasesExpectedDynamicRangeMap: Map<UseCase, DynamicRange> = emptyMap(),
dynamicRangeProfiles: DynamicRangeProfiles? = null,
default10BitProfile: Long? = null,
+ isPreviewStabilizationOn: Boolean = false,
+ hasVideoCapture: Boolean = false
): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
setupCamera(
hardwareLevel = hardwareLevel,
@@ -1494,7 +1526,9 @@
supportedSurfaceCombination.getSuggestedStreamSpecifications(
cameraMode,
attachedSurfaceInfoList,
- useCaseConfigToOutputSizesMap
+ useCaseConfigToOutputSizesMap,
+ isPreviewStabilizationOn,
+ hasVideoCapture
)
val suggestedStreamSpecsForNewUseCases = resultPair.first
val suggestedStreamSpecsForOldSurfaces = resultPair.second
@@ -1515,6 +1549,8 @@
)
.isEqualTo(compareExpectedFps)
}
+ val zslDisabled = suggestedStreamSpecsForNewUseCases[useCaseConfigMap[it]]!!.zslDisabled
+ assertThat(zslDisabled == hasVideoCapture)
}
useCasesExpectedDynamicRangeMap.keys.forEach {
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
index 4d68ac8..4fd20d7 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
@@ -161,7 +161,7 @@
fun surfaceResolutionIsLargestLessThan640x480_when640x480NotPresentInOutputSizes() {
meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWithout640x480)
- meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
+ meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec, null)
assertEquals(Size(320, 240), meteringRepeating.attachedSurfaceResolution)
}
@@ -170,7 +170,7 @@
fun surfaceResolutionIs640x480_when640x480PresentInOutputSizes() {
meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWith640x480)
- meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
+ meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec, null)
assertEquals(Size(640, 480), meteringRepeating.attachedSurfaceResolution)
}
@@ -179,7 +179,7 @@
fun surfaceResolutionFallsBackToMinimum_whenAllOutputSizesLargerThan640x480() {
meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWithoutSmaller)
- meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
+ meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec, null)
assertEquals(Size(1280, 720), meteringRepeating.attachedSurfaceResolution)
}
@@ -188,7 +188,7 @@
fun surfaceResolutionIsLargestWithinPreviewSize_whenAllOutputSizesLessThan640x480() {
meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListSmallerThan640x480)
- meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
+ meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec, null)
assertEquals(Size(320, 480), meteringRepeating.attachedSurfaceResolution)
}
@@ -198,7 +198,7 @@
meteringRepeating =
getMeteringRepeatingAndInitDisplay(dummySizeListNotWithin320x240And640x480)
- meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
+ meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec, null)
assertThat(meteringRepeating.attachedSurfaceResolution).isEqualTo(Size(240, 144))
}
@@ -211,7 +211,7 @@
meteringRepeating =
getMeteringRepeatingAndInitDisplay(dummySizeListNotWithin320x240And640x480)
- meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
+ meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec, null)
assertThat(meteringRepeating.attachedSurfaceResolution).isEqualTo(Size(1280, 720))
}
@@ -223,7 +223,7 @@
meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListSmallerThan320x240)
- meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
+ meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec, null)
assertThat(meteringRepeating.attachedSurfaceResolution).isEqualTo(Size(240, 144))
}
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 df4439a..c900d84 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
@@ -151,7 +151,7 @@
FakeUseCase(FakeUseCaseConfig.Builder().setTargetName("UseCase").useCaseConfig) {
fun setupSessionConfig(sessionConfigBuilder: SessionConfig.Builder) {
- updateSessionConfig(sessionConfigBuilder.build())
+ updateSessionConfig(listOf(sessionConfigBuilder.build()))
notifyActive()
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index c295a48..89ed085 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -719,11 +719,12 @@
bindToCamera(
FakeCamera("0"),
null,
+ null,
getDefaultConfig(
true,
CameraUseCaseAdapter(ApplicationProvider.getApplicationContext())
)
)
- updateSuggestedStreamSpec(StreamSpec.builder(supportedSizes[0]).build())
+ updateSuggestedStreamSpec(StreamSpec.builder(supportedSizes[0]).build(), null)
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/StreamUseCaseTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/StreamUseCaseTest.kt
index f1833ef..8055baf 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/StreamUseCaseTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/StreamUseCaseTest.kt
@@ -29,6 +29,7 @@
import androidx.camera.core.DynamicRange
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.CaptureMode
+import androidx.camera.core.LayoutSettings
import androidx.camera.core.UseCase
import androidx.camera.core.impl.AttachedSurfaceInfo
import androidx.camera.core.impl.CameraMode
@@ -639,7 +640,15 @@
FakeUseCase(FakeUseCaseConfig.Builder().useCaseConfig, CaptureType.IMAGE_CAPTURE),
FakeUseCase(FakeUseCaseConfig.Builder().useCaseConfig, CaptureType.VIDEO_CAPTURE)
)
- val streamSharing = StreamSharing(FakeCamera(), children, useCaseConfigFactory)
+ val streamSharing =
+ StreamSharing(
+ FakeCamera(),
+ null,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ children,
+ useCaseConfigFactory
+ )
val surfaceConfigAttachedSurfaceInfoMap: Map<Int, AttachedSurfaceInfo> = mutableMapOf()
val surfaceConfigUseCaseConfigMap: MutableMap<Int, UseCaseConfig<*>> = mutableMapOf()
surfaceConfigUseCaseConfigMap[0] =
diff --git a/camera/camera-camera2/api/1.4.0-beta03.txt b/camera/camera-camera2/api/1.4.0-beta03.txt
new file mode 100644
index 0000000..2962218
--- /dev/null
+++ b/camera/camera-camera2/api/1.4.0-beta03.txt
@@ -0,0 +1,54 @@
+// Signature format: 4.0
+package androidx.camera.camera2 {
+
+ public final class Camera2Config {
+ method public static androidx.camera.core.CameraXConfig defaultConfig();
+ }
+
+}
+
+package androidx.camera.camera2.interop {
+
+ @SuppressCompatibility @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2CameraControl {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> addCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> clearCaptureRequestOptions();
+ method public static androidx.camera.camera2.interop.Camera2CameraControl from(androidx.camera.core.CameraControl);
+ method public androidx.camera.camera2.interop.CaptureRequestOptions getCaptureRequestOptions();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
+ }
+
+ @SuppressCompatibility @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2CameraInfo {
+ method public static androidx.camera.camera2.interop.Camera2CameraInfo from(androidx.camera.core.CameraInfo);
+ method public <T> T? getCameraCharacteristic(android.hardware.camera2.CameraCharacteristics.Key<T!>);
+ method public String getCameraId();
+ }
+
+ @SuppressCompatibility @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2Interop {
+ }
+
+ public static final class Camera2Interop.Extender<T> {
+ ctor public Camera2Interop.Extender(androidx.camera.core.ExtendableBuilder<T!>);
+ method public <ValueT> androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>, ValueT);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setDeviceStateCallback(android.hardware.camera2.CameraDevice.StateCallback);
+ method @RequiresApi(28) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setPhysicalCameraId(String);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionCaptureCallback(android.hardware.camera2.CameraCaptureSession.CaptureCallback);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionStateCallback(android.hardware.camera2.CameraCaptureSession.StateCallback);
+ method @RequiresApi(33) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setStreamUseCase(long);
+ }
+
+ @SuppressCompatibility @androidx.camera.camera2.interop.ExperimentalCamera2Interop public class CaptureRequestOptions {
+ method public <ValueT> ValueT? getCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
+ }
+
+ public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.interop.CaptureRequestOptions!> {
+ ctor public CaptureRequestOptions.Builder();
+ method public androidx.camera.camera2.interop.CaptureRequestOptions build();
+ method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder clearCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
+ method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder setCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>, ValueT);
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCamera2Interop {
+ }
+
+}
+
diff --git a/camera/camera-camera2/api/res-1.4.0-beta03.txt b/camera/camera-camera2/api/res-1.4.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-camera2/api/res-1.4.0-beta03.txt
diff --git a/camera/camera-camera2/api/restricted_1.4.0-beta03.txt b/camera/camera-camera2/api/restricted_1.4.0-beta03.txt
new file mode 100644
index 0000000..2962218
--- /dev/null
+++ b/camera/camera-camera2/api/restricted_1.4.0-beta03.txt
@@ -0,0 +1,54 @@
+// Signature format: 4.0
+package androidx.camera.camera2 {
+
+ public final class Camera2Config {
+ method public static androidx.camera.core.CameraXConfig defaultConfig();
+ }
+
+}
+
+package androidx.camera.camera2.interop {
+
+ @SuppressCompatibility @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2CameraControl {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> addCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> clearCaptureRequestOptions();
+ method public static androidx.camera.camera2.interop.Camera2CameraControl from(androidx.camera.core.CameraControl);
+ method public androidx.camera.camera2.interop.CaptureRequestOptions getCaptureRequestOptions();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
+ }
+
+ @SuppressCompatibility @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2CameraInfo {
+ method public static androidx.camera.camera2.interop.Camera2CameraInfo from(androidx.camera.core.CameraInfo);
+ method public <T> T? getCameraCharacteristic(android.hardware.camera2.CameraCharacteristics.Key<T!>);
+ method public String getCameraId();
+ }
+
+ @SuppressCompatibility @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2Interop {
+ }
+
+ public static final class Camera2Interop.Extender<T> {
+ ctor public Camera2Interop.Extender(androidx.camera.core.ExtendableBuilder<T!>);
+ method public <ValueT> androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>, ValueT);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setDeviceStateCallback(android.hardware.camera2.CameraDevice.StateCallback);
+ method @RequiresApi(28) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setPhysicalCameraId(String);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionCaptureCallback(android.hardware.camera2.CameraCaptureSession.CaptureCallback);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionStateCallback(android.hardware.camera2.CameraCaptureSession.StateCallback);
+ method @RequiresApi(33) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setStreamUseCase(long);
+ }
+
+ @SuppressCompatibility @androidx.camera.camera2.interop.ExperimentalCamera2Interop public class CaptureRequestOptions {
+ method public <ValueT> ValueT? getCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
+ }
+
+ public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.interop.CaptureRequestOptions!> {
+ ctor public CaptureRequestOptions.Builder();
+ method public androidx.camera.camera2.interop.CaptureRequestOptions build();
+ method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder clearCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
+ method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder setCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>, ValueT);
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCamera2Interop {
+ }
+
+}
+
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index bb80af5..76d5559 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -72,6 +72,7 @@
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
+import androidx.camera.core.LayoutSettings;
import androidx.camera.core.Preview;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CameraCaptureCallback;
@@ -944,7 +945,8 @@
testUseCase.updateSuggestedStreamSpec(StreamSpec.builder(
new Size(640, 480)).setImplementationOptions(
- StreamUseCaseUtil.getStreamSpecImplementationOptions(config)).build());
+ StreamUseCaseUtil.getStreamSpecImplementationOptions(config)).build(),
+ null);
mFakeUseCases.add(testUseCase);
return testUseCase;
}
@@ -962,13 +964,15 @@
new Camera2Interop.Extender<>(configBuilder).setSessionStateCallback(mSessionStateCallback);
UseCaseConfig<?> config = configBuilder.getUseCaseConfig();
- imageCapture.bindToCamera(mCamera2CameraImpl, null, imageCapture.getDefaultConfig(true,
+ imageCapture.bindToCamera(mCamera2CameraImpl, null, null,
+ imageCapture.getDefaultConfig(true,
useCaseConfigFactory));
InstrumentationRegistry.getInstrumentation().runOnMainSync(
() -> imageCapture.updateSuggestedStreamSpec(StreamSpec.builder(
new Size(640, 480)).setImplementationOptions(
- StreamUseCaseUtil.getStreamSpecImplementationOptions(config)).build()));
+ StreamUseCaseUtil.getStreamSpecImplementationOptions(config)).build(),
+ null));
mFakeUseCases.add(imageCapture);
return imageCapture;
@@ -980,7 +984,10 @@
new Camera2UseCaseConfigFactory(ApplicationProvider.getApplicationContext());
StreamSharing streamSharing =
- new StreamSharing(mCamera2CameraImpl, children, useCaseConfigFactory);
+ new StreamSharing(mCamera2CameraImpl, null,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ children, useCaseConfigFactory);
FakeUseCaseConfig.Builder configBuilder =
new FakeUseCaseConfig.Builder().setSessionOptionUnpacker(
@@ -988,13 +995,15 @@
new Camera2Interop.Extender<>(configBuilder).setSessionStateCallback(mSessionStateCallback);
UseCaseConfig<?> config = configBuilder.getUseCaseConfig();
- streamSharing.bindToCamera(mCamera2CameraImpl, null, streamSharing.getDefaultConfig(true,
+ streamSharing.bindToCamera(mCamera2CameraImpl, null, null,
+ streamSharing.getDefaultConfig(true,
useCaseConfigFactory));
InstrumentationRegistry.getInstrumentation().runOnMainSync(
() -> streamSharing.updateSuggestedStreamSpec(StreamSpec.builder(
new Size(640, 480)).setImplementationOptions(
- StreamUseCaseUtil.getStreamSpecImplementationOptions(config)).build()));
+ StreamUseCaseUtil.getStreamSpecImplementationOptions(config)).build(),
+ null));
mFakeUseCases.add(streamSharing);
return streamSharing;
@@ -1369,7 +1378,7 @@
useCase.updateSuggestedStreamSpec(StreamSpec.builder(
new Size(640, 480)).setImplementationOptions(
StreamUseCaseUtil.getStreamSpecImplementationOptions(
- useCase.getCurrentConfig())).build());
+ useCase.getCurrentConfig())).build(), null);
}
private static boolean getDefaultZslDisabled(int templateType) {
@@ -1416,10 +1425,11 @@
mRepeatingCaptureCallback = repeatingCaptureCallback;
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
- bindToCamera(camera, null, null);
+ bindToCamera(camera, null, null, null);
updateSuggestedStreamSpec(StreamSpec.builder(
new Size(640, 480)).setImplementationOptions(
- StreamUseCaseUtil.getStreamSpecImplementationOptions(config)).build());
+ StreamUseCaseUtil.getStreamSpecImplementationOptions(config)).build(),
+ null);
}
public void close() {
@@ -1440,25 +1450,26 @@
@Override
@NonNull
protected StreamSpec onSuggestedStreamSpecUpdated(
- @NonNull StreamSpec suggestedStreamSpec) {
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
if (mDeferrableSurface != null) {
mDeferrableSurface.close();
}
- mDeferrableSurface = createDeferrableSurface(suggestedStreamSpec, mImageFormat);
+ mDeferrableSurface = createDeferrableSurface(primaryStreamSpec, mImageFormat);
mSessionConfigBuilder = SessionConfig.Builder.createFrom(mConfig,
- suggestedStreamSpec.getResolution());
+ primaryStreamSpec.getResolution());
mSessionConfigBuilder.setTemplateType(mTemplate);
mSessionConfigBuilder.addRepeatingCameraCaptureCallback(mRepeatingCaptureCallback);
updateSessionBuilderBySurfaceOption();
- updateSessionConfig(mSessionConfigBuilder.build());
- return suggestedStreamSpec;
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
+ return primaryStreamSpec;
}
public void setSurfaceOption(@NonNull SurfaceOption surfaceOption) {
if (mSurfaceOption != surfaceOption) {
mSurfaceOption = surfaceOption;
updateSessionBuilderBySurfaceOption();
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
}
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
index a0bfd34..0938fa4 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
@@ -41,6 +41,7 @@
import android.view.Surface;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.camera.camera2.Camera2Config;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.camera2.internal.util.SemaphoreReleasingCamera2Callbacks;
@@ -446,10 +447,11 @@
@Override
@NonNull
protected StreamSpec onSuggestedStreamSpecUpdated(
- @NonNull StreamSpec suggestedStreamSpec) {
- createPipeline(suggestedStreamSpec);
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
+ createPipeline(primaryStreamSpec);
notifyActive();
- return suggestedStreamSpec;
+ return primaryStreamSpec;
}
private void createPipeline(StreamSpec streamSpec) {
@@ -490,7 +492,7 @@
// Create new pipeline and it will close the old one.
createPipeline(streamSpec);
});
- updateSessionConfig(builder.build());
+ updateSessionConfig(List.of(builder.build()));
}
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 8302261..62fbd19 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -1250,7 +1250,7 @@
try {
mSupportedSurfaceCombination.getSuggestedStreamSpecifications(cameraMode,
- attachedSurfaces, useCaseConfigToSizeMap, false);
+ attachedSurfaces, useCaseConfigToSizeMap, false, false);
} catch (IllegalArgumentException e) {
debugLog("Surface combination with metering repeating not supported!", e);
return false;
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
index 8eb5953..9aee9a7 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
@@ -155,6 +155,8 @@
* @param newUseCaseConfigsSupportedSizeMap map of configurations of the use cases to the
* supported sizes list that will be given a
* suggested stream specification
+ * @param isPreviewStabilizationOn whether the preview stabilization is enabled.
+ * @param hasVideoCapture whether the use cases has video capture.
* @return map of suggested stream specifications for given use cases
* @throws IllegalStateException if not initialized
* @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
@@ -170,7 +172,8 @@
@NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
@NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap,
- boolean isPreviewStabilizationOn) {
+ boolean isPreviewStabilizationOn,
+ boolean hasVideoCapture) {
Preconditions.checkArgument(!newUseCaseConfigsSupportedSizeMap.isEmpty(),
"No new use cases to be bound.");
@@ -186,6 +189,7 @@
cameraMode,
existingSurfaces,
newUseCaseConfigsSupportedSizeMap,
- isPreviewStabilizationOn);
+ isPreviewStabilizationOn,
+ hasVideoCapture);
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index f12154d..929edd0 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -558,6 +558,7 @@
* @param newUseCaseConfigsSupportedSizeMap newly added UseCaseConfig to supported output
* sizes map.
* @param isPreviewStabilizationOn whether the preview stabilization is enabled.
+ * @param hasVideoCapture whether the use cases has video capture.
* @return the suggested stream specifications, which is a pair of mappings. The first
* mapping is from UseCaseConfig to the suggested stream specification representing new
* UseCases. The second mapping is from attachedSurfaceInfo to the suggested stream
@@ -574,7 +575,8 @@
@CameraMode.Mode int cameraMode,
@NonNull List<AttachedSurfaceInfo> attachedSurfaces,
@NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap,
- boolean isPreviewStabilizationOn) {
+ boolean isPreviewStabilizationOn,
+ boolean hasVideoCapture) {
// Refresh Preview Size based on current display configurations.
refreshPreviewSize();
@@ -787,7 +789,8 @@
resolvedDynamicRanges.get(useCaseConfig)))
.setImplementationOptions(
StreamUseCaseUtil.getStreamSpecImplementationOptions(useCaseConfig)
- );
+ )
+ .setZslDisabled(hasVideoCapture);
if (targetFramerateForDevice != null) {
streamSpecBuilder.setExpectedFrameRateRange(targetFramerateForDevice);
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
index a661164..301336d 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
@@ -42,6 +42,7 @@
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.ImageCapture;
+import androidx.camera.core.LayoutSettings;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.AttachedSurfaceInfo;
import androidx.camera.core.impl.CameraMode;
@@ -481,7 +482,8 @@
UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE));
children.add(new FakeUseCase(new FakeUseCaseConfig.Builder().getUseCaseConfig(),
UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE));
- StreamSharing streamSharing = new StreamSharing(new FakeCamera(), children,
+ StreamSharing streamSharing = new StreamSharing(new FakeCamera(), null,
+ LayoutSettings.DEFAULT, LayoutSettings.DEFAULT, children,
useCaseConfigFactory);
Map<Integer, AttachedSurfaceInfo> surfaceConfigAttachedSurfaceInfoMap =
new HashMap<>();
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index 92b8822..b4059b9 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -1572,6 +1572,34 @@
)
}
+ @Test
+ fun hasVideoCapture_suggestedStreamSpecZslDisabled() {
+ val useCase1 = createUseCase(CaptureType.VIDEO_CAPTURE) // VIDEO
+ val useCase2 = createUseCase(CaptureType.PREVIEW) // PREVIEW
+ val useCaseExpectedResultMap =
+ mutableMapOf<UseCase, Size>().apply {
+ put(useCase1, RECORD_SIZE)
+ put(useCase2, PREVIEW_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ hasVideoCapture = true
+ )
+ }
+
+ @Test
+ fun hasNoVideoCapture_suggestedStreamSpecZslNotDisabled() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PREVIEW
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap =
+ mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, if (Build.VERSION.SDK_INT == 21) RESOLUTION_VGA else PREVIEW_SIZE)
+ put(jpegUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap, hasVideoCapture = false)
+ }
+
private fun getSuggestedSpecsAndVerify(
useCasesExpectedSizeMap: Map<UseCase, Size>,
attachedSurfaceInfoList: List<AttachedSurfaceInfo> = emptyList(),
@@ -1584,6 +1612,8 @@
default10BitProfile: Long? = null,
useCasesExpectedDynamicRangeMap: Map<UseCase, DynamicRange> = emptyMap(),
supportedOutputFormats: IntArray? = null,
+ isPreviewStabilizationOn: Boolean = false,
+ hasVideoCapture: Boolean = false
): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
setupCameraAndInitCameraX(
hardwareLevel = hardwareLevel,
@@ -1608,7 +1638,8 @@
cameraMode,
attachedSurfaceInfoList,
useCaseConfigToOutputSizesMap,
- false
+ isPreviewStabilizationOn,
+ hasVideoCapture
)
val suggestedStreamSpecsForNewUseCases = resultPair.first
val suggestedStreamSpecsForOldSurfaces = resultPair.second
@@ -1629,6 +1660,8 @@
.expectedFrameRateRange == compareExpectedFps
)
}
+ val zslDisabled = suggestedStreamSpecsForNewUseCases[useCaseConfigMap[it]]!!.zslDisabled
+ assertThat(zslDisabled == hasVideoCapture)
}
useCasesExpectedDynamicRangeMap.keys.forEach {
diff --git a/camera/camera-core/api/1.4.0-beta03.txt b/camera/camera-core/api/1.4.0-beta03.txt
new file mode 100644
index 0000000..be07f31
--- /dev/null
+++ b/camera/camera-core/api/1.4.0-beta03.txt
@@ -0,0 +1,742 @@
+// Signature format: 4.0
+package androidx.camera.core {
+
+ public class AspectRatio {
+ field public static final int RATIO_16_9 = 1; // 0x1
+ field public static final int RATIO_4_3 = 0; // 0x0
+ field public static final int RATIO_DEFAULT = -1; // 0xffffffff
+ }
+
+ public interface Camera {
+ method public androidx.camera.core.CameraControl getCameraControl();
+ method public androidx.camera.core.CameraInfo getCameraInfo();
+ }
+
+ public interface CameraControl {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.core.FocusMeteringResult!> startFocusAndMetering(androidx.camera.core.FocusMeteringAction);
+ }
+
+ public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
+ }
+
+ public abstract class CameraEffect {
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.ImageProcessor, androidx.core.util.Consumer<java.lang.Throwable!>);
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor, androidx.core.util.Consumer<java.lang.Throwable!>);
+ method public androidx.core.util.Consumer<java.lang.Throwable!> getErrorListener();
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+ method public int getTargets();
+ field public static final int IMAGE_CAPTURE = 4; // 0x4
+ field public static final int PREVIEW = 1; // 0x1
+ field public static final int VIDEO_CAPTURE = 2; // 0x2
+ }
+
+ public interface CameraFilter {
+ method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
+ }
+
+ public interface CameraInfo {
+ method public androidx.camera.core.CameraSelector getCameraSelector();
+ method public androidx.lifecycle.LiveData<androidx.camera.core.CameraState!> getCameraState();
+ method public androidx.camera.core.ExposureState getExposureState();
+ method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
+ method public default int getLensFacing();
+ method public default java.util.Set<androidx.camera.core.CameraInfo!> getPhysicalCameraInfos();
+ method public int getSensorRotationDegrees();
+ method public int getSensorRotationDegrees(int);
+ method public default java.util.Set<android.util.Range<java.lang.Integer!>!> getSupportedFrameRateRanges();
+ method public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+ method public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
+ method public boolean hasFlashUnit();
+ method public default boolean isFocusMeteringSupported(androidx.camera.core.FocusMeteringAction);
+ method public default boolean isLogicalMultiCameraSupported();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
+ method public static boolean mustPlayShutterSound();
+ method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
+ }
+
+ public final class CameraInfoUnavailableException extends java.lang.Exception {
+ }
+
+ public interface CameraProvider {
+ method public java.util.List<androidx.camera.core.CameraInfo!> getAvailableCameraInfos();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalCameraInfo public default androidx.camera.core.CameraInfo getCameraInfo(androidx.camera.core.CameraSelector);
+ method public boolean hasCamera(androidx.camera.core.CameraSelector) throws androidx.camera.core.CameraInfoUnavailableException;
+ }
+
+ public final class CameraSelector {
+ method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
+ method public String? getPhysicalCameraId();
+ field public static final androidx.camera.core.CameraSelector DEFAULT_BACK_CAMERA;
+ field public static final androidx.camera.core.CameraSelector DEFAULT_FRONT_CAMERA;
+ field public static final int LENS_FACING_BACK = 1; // 0x1
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalLensFacing public static final int LENS_FACING_EXTERNAL = 2; // 0x2
+ field public static final int LENS_FACING_FRONT = 0; // 0x0
+ field public static final int LENS_FACING_UNKNOWN = -1; // 0xffffffff
+ }
+
+ public static final class CameraSelector.Builder {
+ ctor public CameraSelector.Builder();
+ method public androidx.camera.core.CameraSelector.Builder addCameraFilter(androidx.camera.core.CameraFilter);
+ method public androidx.camera.core.CameraSelector build();
+ method public androidx.camera.core.CameraSelector.Builder requireLensFacing(int);
+ method public androidx.camera.core.CameraSelector.Builder setPhysicalCameraId(String);
+ }
+
+ @com.google.auto.value.AutoValue public abstract class CameraState {
+ ctor public CameraState();
+ method public static androidx.camera.core.CameraState create(androidx.camera.core.CameraState.Type);
+ method public static androidx.camera.core.CameraState create(androidx.camera.core.CameraState.Type, androidx.camera.core.CameraState.StateError?);
+ method public abstract androidx.camera.core.CameraState.StateError? getError();
+ method public abstract androidx.camera.core.CameraState.Type getType();
+ field public static final int ERROR_CAMERA_DISABLED = 5; // 0x5
+ field public static final int ERROR_CAMERA_FATAL_ERROR = 6; // 0x6
+ field public static final int ERROR_CAMERA_IN_USE = 2; // 0x2
+ field public static final int ERROR_DO_NOT_DISTURB_MODE_ENABLED = 7; // 0x7
+ field public static final int ERROR_MAX_CAMERAS_IN_USE = 1; // 0x1
+ field public static final int ERROR_OTHER_RECOVERABLE_ERROR = 3; // 0x3
+ field public static final int ERROR_STREAM_CONFIG = 4; // 0x4
+ }
+
+ public enum CameraState.ErrorType {
+ enum_constant public static final androidx.camera.core.CameraState.ErrorType CRITICAL;
+ enum_constant public static final androidx.camera.core.CameraState.ErrorType RECOVERABLE;
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class CameraState.StateError {
+ ctor public CameraState.StateError();
+ method public static androidx.camera.core.CameraState.StateError create(int);
+ method public static androidx.camera.core.CameraState.StateError create(int, Throwable?);
+ method public abstract Throwable? getCause();
+ method public abstract int getCode();
+ method public androidx.camera.core.CameraState.ErrorType getType();
+ }
+
+ public enum CameraState.Type {
+ enum_constant public static final androidx.camera.core.CameraState.Type CLOSED;
+ enum_constant public static final androidx.camera.core.CameraState.Type CLOSING;
+ enum_constant public static final androidx.camera.core.CameraState.Type OPEN;
+ enum_constant public static final androidx.camera.core.CameraState.Type OPENING;
+ enum_constant public static final androidx.camera.core.CameraState.Type PENDING_OPEN;
+ }
+
+ public class CameraUnavailableException extends java.lang.Exception {
+ ctor public CameraUnavailableException(int);
+ ctor public CameraUnavailableException(int, String?);
+ ctor public CameraUnavailableException(int, String?, Throwable?);
+ ctor public CameraUnavailableException(int, Throwable?);
+ method public int getReason();
+ field public static final int CAMERA_DISABLED = 1; // 0x1
+ field public static final int CAMERA_DISCONNECTED = 2; // 0x2
+ field public static final int CAMERA_ERROR = 3; // 0x3
+ field public static final int CAMERA_IN_USE = 4; // 0x4
+ field public static final int CAMERA_MAX_IN_USE = 5; // 0x5
+ field public static final int CAMERA_UNAVAILABLE_DO_NOT_DISTURB = 6; // 0x6
+ field public static final int CAMERA_UNKNOWN_ERROR = 0; // 0x0
+ }
+
+ public final class CameraXConfig {
+ method public androidx.camera.core.CameraSelector? getAvailableCamerasLimiter(androidx.camera.core.CameraSelector?);
+ method public java.util.concurrent.Executor? getCameraExecutor(java.util.concurrent.Executor?);
+ method public long getCameraOpenRetryMaxTimeoutInMillisWhileResuming();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public androidx.camera.core.RetryPolicy getCameraProviderInitRetryPolicy();
+ method public int getMinimumLoggingLevel();
+ method public android.os.Handler? getSchedulerHandler(android.os.Handler?);
+ }
+
+ public static final class CameraXConfig.Builder {
+ method public androidx.camera.core.CameraXConfig build();
+ method public static androidx.camera.core.CameraXConfig.Builder fromConfig(androidx.camera.core.CameraXConfig);
+ method public androidx.camera.core.CameraXConfig.Builder setAvailableCamerasLimiter(androidx.camera.core.CameraSelector);
+ method public androidx.camera.core.CameraXConfig.Builder setCameraExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.CameraXConfig.Builder setCameraOpenRetryMaxTimeoutInMillisWhileResuming(long);
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public androidx.camera.core.CameraXConfig.Builder setCameraProviderInitRetryPolicy(androidx.camera.core.RetryPolicy);
+ method public androidx.camera.core.CameraXConfig.Builder setMinimumLoggingLevel(@IntRange(from=android.util.Log.DEBUG, to=android.util.Log.ERROR) int);
+ method public androidx.camera.core.CameraXConfig.Builder setSchedulerHandler(android.os.Handler);
+ }
+
+ public static interface CameraXConfig.Provider {
+ method public androidx.camera.core.CameraXConfig getCameraXConfig();
+ }
+
+ public class ConcurrentCamera {
+ ctor public ConcurrentCamera(java.util.List<androidx.camera.core.Camera!>);
+ method public java.util.List<androidx.camera.core.Camera!> getCameras();
+ }
+
+ public static final class ConcurrentCamera.SingleCameraConfig {
+ ctor public ConcurrentCamera.SingleCameraConfig(androidx.camera.core.CameraSelector, androidx.camera.core.UseCaseGroup, androidx.lifecycle.LifecycleOwner);
+ method public androidx.camera.core.CameraSelector getCameraSelector();
+ method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
+ method public androidx.camera.core.UseCaseGroup getUseCaseGroup();
+ }
+
+ public final class DisplayOrientedMeteringPointFactory extends androidx.camera.core.MeteringPointFactory {
+ ctor public DisplayOrientedMeteringPointFactory(android.view.Display, androidx.camera.core.CameraInfo, float, float);
+ }
+
+ public final class DynamicRange {
+ ctor public DynamicRange(int, int);
+ method public int getBitDepth();
+ method public int getEncoding();
+ field public static final int BIT_DEPTH_10_BIT = 10; // 0xa
+ field public static final int BIT_DEPTH_8_BIT = 8; // 0x8
+ field public static final int BIT_DEPTH_UNSPECIFIED = 0; // 0x0
+ field public static final androidx.camera.core.DynamicRange DOLBY_VISION_10_BIT;
+ field public static final androidx.camera.core.DynamicRange DOLBY_VISION_8_BIT;
+ field public static final int ENCODING_DOLBY_VISION = 6; // 0x6
+ field public static final int ENCODING_HDR10 = 4; // 0x4
+ field public static final int ENCODING_HDR10_PLUS = 5; // 0x5
+ field public static final int ENCODING_HDR_UNSPECIFIED = 2; // 0x2
+ field public static final int ENCODING_HLG = 3; // 0x3
+ field public static final int ENCODING_SDR = 1; // 0x1
+ field public static final int ENCODING_UNSPECIFIED = 0; // 0x0
+ field public static final androidx.camera.core.DynamicRange HDR10_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HDR10_PLUS_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HDR_UNSPECIFIED_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HLG_10_BIT;
+ field public static final androidx.camera.core.DynamicRange SDR;
+ field public static final androidx.camera.core.DynamicRange UNSPECIFIED;
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraInfo {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalImageCaptureOutputFormat {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLensFacing {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalMirrorMode {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalRetryPolicy {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseApi {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalZeroShutterLag {
+ }
+
+ public interface ExposureState {
+ method public int getExposureCompensationIndex();
+ method public android.util.Range<java.lang.Integer!> getExposureCompensationRange();
+ method public android.util.Rational getExposureCompensationStep();
+ method public boolean isExposureCompensationSupported();
+ }
+
+ public interface ExtendableBuilder<T> {
+ method public T build();
+ }
+
+ public final class FocusMeteringAction {
+ method public long getAutoCancelDurationInMillis();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAe();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAf();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAwb();
+ method public boolean isAutoCancelEnabled();
+ field public static final int FLAG_AE = 2; // 0x2
+ field public static final int FLAG_AF = 1; // 0x1
+ field public static final int FLAG_AWB = 4; // 0x4
+ }
+
+ public static class FocusMeteringAction.Builder {
+ ctor public FocusMeteringAction.Builder(androidx.camera.core.MeteringPoint);
+ ctor public FocusMeteringAction.Builder(androidx.camera.core.MeteringPoint, int);
+ method public androidx.camera.core.FocusMeteringAction.Builder addPoint(androidx.camera.core.MeteringPoint);
+ method public androidx.camera.core.FocusMeteringAction.Builder addPoint(androidx.camera.core.MeteringPoint, int);
+ method public androidx.camera.core.FocusMeteringAction build();
+ method public androidx.camera.core.FocusMeteringAction.Builder disableAutoCancel();
+ method public androidx.camera.core.FocusMeteringAction.Builder setAutoCancelDuration(@IntRange(from=1) long, java.util.concurrent.TimeUnit);
+ }
+
+ public final class FocusMeteringResult {
+ method public boolean isFocusSuccessful();
+ }
+
+ public final class ImageAnalysis extends androidx.camera.core.UseCase {
+ method public void clearAnalyzer();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalUseCaseApi public java.util.concurrent.Executor? getBackgroundExecutor();
+ method public int getBackpressureStrategy();
+ method public int getImageQueueDepth();
+ method public int getOutputImageFormat();
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public int getTargetRotation();
+ method public boolean isOutputImageRotationEnabled();
+ method public void setAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+ method public void setTargetRotation(int);
+ field public static final int COORDINATE_SYSTEM_ORIGINAL = 0; // 0x0
+ field public static final int COORDINATE_SYSTEM_SENSOR = 2; // 0x2
+ field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
+ field public static final int OUTPUT_IMAGE_FORMAT_RGBA_8888 = 2; // 0x2
+ field public static final int OUTPUT_IMAGE_FORMAT_YUV_420_888 = 1; // 0x1
+ field public static final int STRATEGY_BLOCK_PRODUCER = 1; // 0x1
+ field public static final int STRATEGY_KEEP_ONLY_LATEST = 0; // 0x0
+ }
+
+ public static interface ImageAnalysis.Analyzer {
+ method public void analyze(androidx.camera.core.ImageProxy);
+ method public default android.util.Size? getDefaultTargetResolution();
+ method public default int getTargetCoordinateSystem();
+ method public default void updateTransform(android.graphics.Matrix?);
+ }
+
+ public static final class ImageAnalysis.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageAnalysis!> {
+ ctor public ImageAnalysis.Builder();
+ method public androidx.camera.core.ImageAnalysis build();
+ method public androidx.camera.core.ImageAnalysis.Builder setBackgroundExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.ImageAnalysis.Builder setBackpressureStrategy(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setImageQueueDepth(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setOutputImageFormat(int);
+ method @RequiresApi(23) public androidx.camera.core.ImageAnalysis.Builder setOutputImageRotationEnabled(boolean);
+ method public androidx.camera.core.ImageAnalysis.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method @Deprecated public androidx.camera.core.ImageAnalysis.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.ImageAnalysis.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.ImageAnalysis.Builder setTargetRotation(int);
+ }
+
+ public final class ImageCapture extends androidx.camera.core.UseCase {
+ method public int getCaptureMode();
+ method public int getFlashMode();
+ method public static androidx.camera.core.ImageCaptureCapabilities getImageCaptureCapabilities(androidx.camera.core.CameraInfo);
+ method @IntRange(from=1, to=100) public int getJpegQuality();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public int getOutputFormat();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getPostviewResolutionSelector();
+ method public androidx.camera.core.ImageCaptureLatencyEstimate getRealtimeCaptureLatencyEstimate();
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public androidx.camera.core.ImageCapture.ScreenFlash? getScreenFlash();
+ method public int getTargetRotation();
+ method public boolean isPostviewEnabled();
+ method public void setCropAspectRatio(android.util.Rational);
+ method public void setFlashMode(int);
+ method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
+ method public void setTargetRotation(int);
+ method public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
+ method public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
+ field public static final int CAPTURE_MODE_MAXIMIZE_QUALITY = 0; // 0x0
+ field public static final int CAPTURE_MODE_MINIMIZE_LATENCY = 1; // 0x1
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public static final int CAPTURE_MODE_ZERO_SHUTTER_LAG = 2; // 0x2
+ field public static final int ERROR_CAMERA_CLOSED = 3; // 0x3
+ field public static final int ERROR_CAPTURE_FAILED = 2; // 0x2
+ field public static final int ERROR_FILE_IO = 1; // 0x1
+ field public static final int ERROR_INVALID_CAMERA = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 0; // 0x0
+ field public static final int FLASH_MODE_AUTO = 0; // 0x0
+ field public static final int FLASH_MODE_OFF = 2; // 0x2
+ field public static final int FLASH_MODE_ON = 1; // 0x1
+ field public static final int FLASH_MODE_SCREEN = 3; // 0x3
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG = 0; // 0x0
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG_ULTRA_HDR = 1; // 0x1
+ }
+
+ public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture!> {
+ ctor public ImageCapture.Builder();
+ method public androidx.camera.core.ImageCapture build();
+ method public androidx.camera.core.ImageCapture.Builder setCaptureMode(int);
+ method public androidx.camera.core.ImageCapture.Builder setFlashMode(int);
+ method public androidx.camera.core.ImageCapture.Builder setIoExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.ImageCapture.Builder setJpegQuality(@IntRange(from=1, to=100) int);
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public androidx.camera.core.ImageCapture.Builder setOutputFormat(int);
+ method public androidx.camera.core.ImageCapture.Builder setPostviewEnabled(boolean);
+ method public androidx.camera.core.ImageCapture.Builder setPostviewResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method public androidx.camera.core.ImageCapture.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method public androidx.camera.core.ImageCapture.Builder setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash);
+ method @Deprecated public androidx.camera.core.ImageCapture.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.ImageCapture.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.ImageCapture.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.ImageCapture.Builder setTargetRotation(int);
+ }
+
+ public static final class ImageCapture.Metadata {
+ ctor public ImageCapture.Metadata();
+ method public android.location.Location? getLocation();
+ method public boolean isReversedHorizontal();
+ method public boolean isReversedVertical();
+ method public void setLocation(android.location.Location?);
+ method public void setReversedHorizontal(boolean);
+ method public void setReversedVertical(boolean);
+ }
+
+ public abstract static class ImageCapture.OnImageCapturedCallback {
+ ctor public ImageCapture.OnImageCapturedCallback();
+ method public void onCaptureProcessProgressed(int);
+ method public void onCaptureStarted();
+ method public void onCaptureSuccess(androidx.camera.core.ImageProxy);
+ method public void onError(androidx.camera.core.ImageCaptureException);
+ method public void onPostviewBitmapAvailable(android.graphics.Bitmap);
+ }
+
+ public static interface ImageCapture.OnImageSavedCallback {
+ method public default void onCaptureProcessProgressed(int);
+ method public default void onCaptureStarted();
+ method public void onError(androidx.camera.core.ImageCaptureException);
+ method public void onImageSaved(androidx.camera.core.ImageCapture.OutputFileResults);
+ method public default void onPostviewBitmapAvailable(android.graphics.Bitmap);
+ }
+
+ public static final class ImageCapture.OutputFileOptions {
+ }
+
+ public static final class ImageCapture.OutputFileOptions.Builder {
+ ctor public ImageCapture.OutputFileOptions.Builder(android.content.ContentResolver, android.net.Uri, android.content.ContentValues);
+ ctor public ImageCapture.OutputFileOptions.Builder(java.io.File);
+ ctor public ImageCapture.OutputFileOptions.Builder(java.io.OutputStream);
+ method public androidx.camera.core.ImageCapture.OutputFileOptions build();
+ method public androidx.camera.core.ImageCapture.OutputFileOptions.Builder setMetadata(androidx.camera.core.ImageCapture.Metadata);
+ }
+
+ public static class ImageCapture.OutputFileResults {
+ method public android.net.Uri? getSavedUri();
+ }
+
+ public static interface ImageCapture.ScreenFlash {
+ method @UiThread public void apply(long, androidx.camera.core.ImageCapture.ScreenFlashListener);
+ method @UiThread public void clear();
+ }
+
+ public static interface ImageCapture.ScreenFlashListener {
+ method public void onCompleted();
+ }
+
+ public interface ImageCaptureCapabilities {
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public java.util.Set<java.lang.Integer!> getSupportedOutputFormats();
+ method public boolean isCaptureProcessProgressSupported();
+ method public boolean isPostviewSupported();
+ }
+
+ public class ImageCaptureException extends java.lang.Exception {
+ ctor public ImageCaptureException(int, String, Throwable?);
+ method public int getImageCaptureError();
+ }
+
+ public final class ImageCaptureExtKt {
+ method public static suspend Object? takePicture(androidx.camera.core.ImageCapture, androidx.camera.core.ImageCapture.OutputFileOptions outputFileOptions, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onCaptureStarted, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit>? onCaptureProcessProgressed, optional kotlin.jvm.functions.Function1<? super android.graphics.Bitmap,kotlin.Unit>? onPostviewBitmapAvailable, kotlin.coroutines.Continuation<? super androidx.camera.core.ImageCapture.OutputFileResults>);
+ method public static suspend Object? takePicture(androidx.camera.core.ImageCapture, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onCaptureStarted, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit>? onCaptureProcessProgressed, optional kotlin.jvm.functions.Function1<? super android.graphics.Bitmap,kotlin.Unit>? onPostviewBitmapAvailable, kotlin.coroutines.Continuation<? super androidx.camera.core.ImageProxy>);
+ }
+
+ public class ImageCaptureLatencyEstimate {
+ ctor public ImageCaptureLatencyEstimate(long, long);
+ method public long getCaptureLatencyMillis();
+ method public long getProcessingLatencyMillis();
+ method public long getTotalCaptureLatencyMillis();
+ field public static final long UNDEFINED_CAPTURE_LATENCY = -1L; // 0xffffffffffffffffL
+ field public static final androidx.camera.core.ImageCaptureLatencyEstimate UNDEFINED_IMAGE_CAPTURE_LATENCY;
+ field public static final long UNDEFINED_PROCESSING_LATENCY = -1L; // 0xffffffffffffffffL
+ }
+
+ public interface ImageInfo {
+ method public int getRotationDegrees();
+ method public default android.graphics.Matrix getSensorToBufferTransformMatrix();
+ method public long getTimestamp();
+ }
+
+ public interface ImageProcessor {
+ method public androidx.camera.core.ImageProcessor.Response process(androidx.camera.core.ImageProcessor.Request) throws androidx.camera.core.ProcessingException;
+ }
+
+ public static interface ImageProcessor.Request {
+ method public androidx.camera.core.ImageProxy getInputImage();
+ method public int getOutputFormat();
+ }
+
+ public static interface ImageProcessor.Response {
+ method public androidx.camera.core.ImageProxy getOutputImage();
+ }
+
+ public interface ImageProxy extends java.lang.AutoCloseable {
+ method public void close();
+ method public android.graphics.Rect getCropRect();
+ method public int getFormat();
+ method public int getHeight();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalGetImage public android.media.Image? getImage();
+ method public androidx.camera.core.ImageInfo getImageInfo();
+ method public androidx.camera.core.ImageProxy.PlaneProxy![] getPlanes();
+ method public int getWidth();
+ method public void setCropRect(android.graphics.Rect?);
+ method public default android.graphics.Bitmap toBitmap();
+ }
+
+ public static interface ImageProxy.PlaneProxy {
+ method public java.nio.ByteBuffer getBuffer();
+ method public int getPixelStride();
+ method public int getRowStride();
+ }
+
+ public class InitializationException extends java.lang.Exception {
+ ctor public InitializationException(String?);
+ ctor public InitializationException(String?, Throwable?);
+ ctor public InitializationException(Throwable?);
+ }
+
+ public class MeteringPoint {
+ method public float getSize();
+ }
+
+ public abstract class MeteringPointFactory {
+ method public final androidx.camera.core.MeteringPoint createPoint(float, float);
+ method public final androidx.camera.core.MeteringPoint createPoint(float, float, float);
+ method public static float getDefaultPointSize();
+ }
+
+ public class MirrorMode {
+ field public static final int MIRROR_MODE_OFF = 0; // 0x0
+ field public static final int MIRROR_MODE_ON = 1; // 0x1
+ field public static final int MIRROR_MODE_ON_FRONT_ONLY = 2; // 0x2
+ }
+
+ public final class Preview extends androidx.camera.core.UseCase {
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public static androidx.camera.core.PreviewCapabilities getPreviewCapabilities(androidx.camera.core.CameraInfo);
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public android.util.Range<java.lang.Integer!> getTargetFrameRate();
+ method public int getTargetRotation();
+ method public boolean isPreviewStabilizationEnabled();
+ method @UiThread public void setSurfaceProvider(androidx.camera.core.Preview.SurfaceProvider?);
+ method @UiThread public void setSurfaceProvider(java.util.concurrent.Executor, androidx.camera.core.Preview.SurfaceProvider?);
+ method public void setTargetRotation(int);
+ }
+
+ public static final class Preview.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.Preview!> {
+ ctor public Preview.Builder();
+ method public androidx.camera.core.Preview build();
+ method public androidx.camera.core.Preview.Builder setDynamicRange(androidx.camera.core.DynamicRange);
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalMirrorMode public androidx.camera.core.Preview.Builder setMirrorMode(int);
+ method public androidx.camera.core.Preview.Builder setPreviewStabilizationEnabled(boolean);
+ method public androidx.camera.core.Preview.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method @Deprecated public androidx.camera.core.Preview.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.Preview.Builder setTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method public androidx.camera.core.Preview.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.Preview.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.Preview.Builder setTargetRotation(int);
+ }
+
+ public static interface Preview.SurfaceProvider {
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ }
+
+ public interface PreviewCapabilities {
+ method public boolean isStabilizationSupported();
+ }
+
+ public class ProcessingException extends java.lang.Exception {
+ ctor public ProcessingException();
+ }
+
+ public class ResolutionInfo {
+ ctor public ResolutionInfo(android.util.Size, android.graphics.Rect, int);
+ method public android.graphics.Rect getCropRect();
+ method public android.util.Size getResolution();
+ method public int getRotationDegrees();
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public interface RetryPolicy {
+ method public static long getDefaultRetryTimeoutInMillis();
+ method public default long getTimeoutInMillis();
+ method public androidx.camera.core.RetryPolicy.RetryConfig onRetryDecisionRequested(androidx.camera.core.RetryPolicy.ExecutionState);
+ field public static final androidx.camera.core.RetryPolicy DEFAULT;
+ field public static final androidx.camera.core.RetryPolicy NEVER;
+ field public static final androidx.camera.core.RetryPolicy RETRY_UNAVAILABLE_CAMERA;
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.Builder {
+ ctor public RetryPolicy.Builder(androidx.camera.core.RetryPolicy);
+ method public androidx.camera.core.RetryPolicy build();
+ method public androidx.camera.core.RetryPolicy.Builder setTimeoutInMillis(long);
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static interface RetryPolicy.ExecutionState {
+ method public Throwable? getCause();
+ method public long getExecutedTimeInMillis();
+ method public int getNumOfAttempts();
+ method public int getStatus();
+ field public static final int STATUS_CAMERA_UNAVAILABLE = 2; // 0x2
+ field public static final int STATUS_CONFIGURATION_FAIL = 1; // 0x1
+ field public static final int STATUS_UNKNOWN_ERROR = 0; // 0x0
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig {
+ method public static long getDefaultRetryDelayInMillis();
+ method public long getRetryDelayInMillis();
+ method public boolean shouldRetry();
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig DEFAULT_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig MINI_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig NOT_RETRY;
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig.Builder {
+ ctor public RetryPolicy.RetryConfig.Builder();
+ method public androidx.camera.core.RetryPolicy.RetryConfig build();
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setRetryDelayInMillis(@IntRange(from=100, to=2000) long);
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setShouldRetry(boolean);
+ }
+
+ public class SurfaceOrientedMeteringPointFactory extends androidx.camera.core.MeteringPointFactory {
+ ctor public SurfaceOrientedMeteringPointFactory(float, float);
+ ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
+ }
+
+ public interface SurfaceOutput extends java.io.Closeable {
+ method public void close();
+ method public default android.graphics.Matrix getSensorToBufferTransform();
+ method public android.util.Size getSize();
+ method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+ method public int getTargets();
+ method public void updateTransformMatrix(float[], float[]);
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+ method public abstract int getEventCode();
+ method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+ field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+ }
+
+ public interface SurfaceProcessor {
+ method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+ method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+ }
+
+ public final class SurfaceRequest {
+ method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
+ method public void clearTransformationInfoListener();
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public android.util.Size getResolution();
+ method public boolean invalidate();
+ method public void provideSurface(android.view.Surface, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceRequest.Result!>);
+ method public void setTransformationInfoListener(java.util.concurrent.Executor, androidx.camera.core.SurfaceRequest.TransformationInfoListener);
+ method public boolean willNotProvideSurface();
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceRequest.Result {
+ method public abstract int getResultCode();
+ method public abstract android.view.Surface getSurface();
+ field public static final int RESULT_INVALID_SURFACE = 2; // 0x2
+ field public static final int RESULT_REQUEST_CANCELLED = 1; // 0x1
+ field public static final int RESULT_SURFACE_ALREADY_PROVIDED = 3; // 0x3
+ field public static final int RESULT_SURFACE_USED_SUCCESSFULLY = 0; // 0x0
+ field public static final int RESULT_WILL_NOT_PROVIDE_SURFACE = 4; // 0x4
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceRequest.TransformationInfo {
+ method public abstract android.graphics.Rect getCropRect();
+ method public abstract int getRotationDegrees();
+ method public abstract android.graphics.Matrix getSensorToBufferTransform();
+ method public abstract boolean hasCameraTransform();
+ method public abstract boolean isMirroring();
+ }
+
+ public static interface SurfaceRequest.TransformationInfoListener {
+ method public void onTransformationInfoUpdate(androidx.camera.core.SurfaceRequest.TransformationInfo);
+ }
+
+ public class TorchState {
+ field public static final int OFF = 0; // 0x0
+ field public static final int ON = 1; // 0x1
+ }
+
+ public abstract class UseCase {
+ method public static int snapToSurfaceRotation(@IntRange(from=0, to=359) int);
+ }
+
+ public final class UseCaseGroup {
+ method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
+ method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
+ method public androidx.camera.core.ViewPort? getViewPort();
+ }
+
+ public static final class UseCaseGroup.Builder {
+ ctor public UseCaseGroup.Builder();
+ method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
+ method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
+ method public androidx.camera.core.UseCaseGroup build();
+ method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
+ }
+
+ public final class ViewPort {
+ method public android.util.Rational getAspectRatio();
+ method public int getLayoutDirection();
+ method public int getRotation();
+ method public int getScaleType();
+ field public static final int FILL_CENTER = 1; // 0x1
+ field public static final int FILL_END = 2; // 0x2
+ field public static final int FILL_START = 0; // 0x0
+ field public static final int FIT = 3; // 0x3
+ }
+
+ public static final class ViewPort.Builder {
+ ctor public ViewPort.Builder(android.util.Rational, int);
+ method public androidx.camera.core.ViewPort build();
+ method public androidx.camera.core.ViewPort.Builder setLayoutDirection(int);
+ method public androidx.camera.core.ViewPort.Builder setScaleType(int);
+ }
+
+ public interface ZoomState {
+ method public float getLinearZoom();
+ method public float getMaxZoomRatio();
+ method public float getMinZoomRatio();
+ method public float getZoomRatio();
+ }
+
+}
+
+package androidx.camera.core.resolutionselector {
+
+ public final class AspectRatioStrategy {
+ ctor public AspectRatioStrategy(int, int);
+ method public int getFallbackRule();
+ method public int getPreferredAspectRatio();
+ field public static final int FALLBACK_RULE_AUTO = 1; // 0x1
+ field public static final int FALLBACK_RULE_NONE = 0; // 0x0
+ field public static final androidx.camera.core.resolutionselector.AspectRatioStrategy RATIO_16_9_FALLBACK_AUTO_STRATEGY;
+ field public static final androidx.camera.core.resolutionselector.AspectRatioStrategy RATIO_4_3_FALLBACK_AUTO_STRATEGY;
+ }
+
+ public interface ResolutionFilter {
+ method public java.util.List<android.util.Size!> filter(java.util.List<android.util.Size!>, int);
+ }
+
+ public final class ResolutionSelector {
+ method public int getAllowedResolutionMode();
+ method public androidx.camera.core.resolutionselector.AspectRatioStrategy getAspectRatioStrategy();
+ method public androidx.camera.core.resolutionselector.ResolutionFilter? getResolutionFilter();
+ method public androidx.camera.core.resolutionselector.ResolutionStrategy? getResolutionStrategy();
+ field public static final int PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION = 0; // 0x0
+ field public static final int PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE = 1; // 0x1
+ }
+
+ public static final class ResolutionSelector.Builder {
+ ctor public ResolutionSelector.Builder();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector build();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setAllowedResolutionMode(int);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setAspectRatioStrategy(androidx.camera.core.resolutionselector.AspectRatioStrategy);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setResolutionFilter(androidx.camera.core.resolutionselector.ResolutionFilter);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setResolutionStrategy(androidx.camera.core.resolutionselector.ResolutionStrategy);
+ }
+
+ public final class ResolutionStrategy {
+ ctor public ResolutionStrategy(android.util.Size, int);
+ method public android.util.Size? getBoundSize();
+ method public int getFallbackRule();
+ field public static final int FALLBACK_RULE_CLOSEST_HIGHER = 2; // 0x2
+ field public static final int FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER = 1; // 0x1
+ field public static final int FALLBACK_RULE_CLOSEST_LOWER = 4; // 0x4
+ field public static final int FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER = 3; // 0x3
+ field public static final int FALLBACK_RULE_NONE = 0; // 0x0
+ field public static final androidx.camera.core.resolutionselector.ResolutionStrategy HIGHEST_AVAILABLE_STRATEGY;
+ }
+
+}
+
diff --git a/camera/camera-core/api/res-1.4.0-beta03.txt b/camera/camera-core/api/res-1.4.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-core/api/res-1.4.0-beta03.txt
diff --git a/camera/camera-core/api/restricted_1.4.0-beta03.txt b/camera/camera-core/api/restricted_1.4.0-beta03.txt
new file mode 100644
index 0000000..be07f31
--- /dev/null
+++ b/camera/camera-core/api/restricted_1.4.0-beta03.txt
@@ -0,0 +1,742 @@
+// Signature format: 4.0
+package androidx.camera.core {
+
+ public class AspectRatio {
+ field public static final int RATIO_16_9 = 1; // 0x1
+ field public static final int RATIO_4_3 = 0; // 0x0
+ field public static final int RATIO_DEFAULT = -1; // 0xffffffff
+ }
+
+ public interface Camera {
+ method public androidx.camera.core.CameraControl getCameraControl();
+ method public androidx.camera.core.CameraInfo getCameraInfo();
+ }
+
+ public interface CameraControl {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.core.FocusMeteringResult!> startFocusAndMetering(androidx.camera.core.FocusMeteringAction);
+ }
+
+ public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
+ }
+
+ public abstract class CameraEffect {
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.ImageProcessor, androidx.core.util.Consumer<java.lang.Throwable!>);
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor, androidx.core.util.Consumer<java.lang.Throwable!>);
+ method public androidx.core.util.Consumer<java.lang.Throwable!> getErrorListener();
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+ method public int getTargets();
+ field public static final int IMAGE_CAPTURE = 4; // 0x4
+ field public static final int PREVIEW = 1; // 0x1
+ field public static final int VIDEO_CAPTURE = 2; // 0x2
+ }
+
+ public interface CameraFilter {
+ method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
+ }
+
+ public interface CameraInfo {
+ method public androidx.camera.core.CameraSelector getCameraSelector();
+ method public androidx.lifecycle.LiveData<androidx.camera.core.CameraState!> getCameraState();
+ method public androidx.camera.core.ExposureState getExposureState();
+ method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
+ method public default int getLensFacing();
+ method public default java.util.Set<androidx.camera.core.CameraInfo!> getPhysicalCameraInfos();
+ method public int getSensorRotationDegrees();
+ method public int getSensorRotationDegrees(int);
+ method public default java.util.Set<android.util.Range<java.lang.Integer!>!> getSupportedFrameRateRanges();
+ method public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+ method public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
+ method public boolean hasFlashUnit();
+ method public default boolean isFocusMeteringSupported(androidx.camera.core.FocusMeteringAction);
+ method public default boolean isLogicalMultiCameraSupported();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
+ method public static boolean mustPlayShutterSound();
+ method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
+ }
+
+ public final class CameraInfoUnavailableException extends java.lang.Exception {
+ }
+
+ public interface CameraProvider {
+ method public java.util.List<androidx.camera.core.CameraInfo!> getAvailableCameraInfos();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalCameraInfo public default androidx.camera.core.CameraInfo getCameraInfo(androidx.camera.core.CameraSelector);
+ method public boolean hasCamera(androidx.camera.core.CameraSelector) throws androidx.camera.core.CameraInfoUnavailableException;
+ }
+
+ public final class CameraSelector {
+ method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
+ method public String? getPhysicalCameraId();
+ field public static final androidx.camera.core.CameraSelector DEFAULT_BACK_CAMERA;
+ field public static final androidx.camera.core.CameraSelector DEFAULT_FRONT_CAMERA;
+ field public static final int LENS_FACING_BACK = 1; // 0x1
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalLensFacing public static final int LENS_FACING_EXTERNAL = 2; // 0x2
+ field public static final int LENS_FACING_FRONT = 0; // 0x0
+ field public static final int LENS_FACING_UNKNOWN = -1; // 0xffffffff
+ }
+
+ public static final class CameraSelector.Builder {
+ ctor public CameraSelector.Builder();
+ method public androidx.camera.core.CameraSelector.Builder addCameraFilter(androidx.camera.core.CameraFilter);
+ method public androidx.camera.core.CameraSelector build();
+ method public androidx.camera.core.CameraSelector.Builder requireLensFacing(int);
+ method public androidx.camera.core.CameraSelector.Builder setPhysicalCameraId(String);
+ }
+
+ @com.google.auto.value.AutoValue public abstract class CameraState {
+ ctor public CameraState();
+ method public static androidx.camera.core.CameraState create(androidx.camera.core.CameraState.Type);
+ method public static androidx.camera.core.CameraState create(androidx.camera.core.CameraState.Type, androidx.camera.core.CameraState.StateError?);
+ method public abstract androidx.camera.core.CameraState.StateError? getError();
+ method public abstract androidx.camera.core.CameraState.Type getType();
+ field public static final int ERROR_CAMERA_DISABLED = 5; // 0x5
+ field public static final int ERROR_CAMERA_FATAL_ERROR = 6; // 0x6
+ field public static final int ERROR_CAMERA_IN_USE = 2; // 0x2
+ field public static final int ERROR_DO_NOT_DISTURB_MODE_ENABLED = 7; // 0x7
+ field public static final int ERROR_MAX_CAMERAS_IN_USE = 1; // 0x1
+ field public static final int ERROR_OTHER_RECOVERABLE_ERROR = 3; // 0x3
+ field public static final int ERROR_STREAM_CONFIG = 4; // 0x4
+ }
+
+ public enum CameraState.ErrorType {
+ enum_constant public static final androidx.camera.core.CameraState.ErrorType CRITICAL;
+ enum_constant public static final androidx.camera.core.CameraState.ErrorType RECOVERABLE;
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class CameraState.StateError {
+ ctor public CameraState.StateError();
+ method public static androidx.camera.core.CameraState.StateError create(int);
+ method public static androidx.camera.core.CameraState.StateError create(int, Throwable?);
+ method public abstract Throwable? getCause();
+ method public abstract int getCode();
+ method public androidx.camera.core.CameraState.ErrorType getType();
+ }
+
+ public enum CameraState.Type {
+ enum_constant public static final androidx.camera.core.CameraState.Type CLOSED;
+ enum_constant public static final androidx.camera.core.CameraState.Type CLOSING;
+ enum_constant public static final androidx.camera.core.CameraState.Type OPEN;
+ enum_constant public static final androidx.camera.core.CameraState.Type OPENING;
+ enum_constant public static final androidx.camera.core.CameraState.Type PENDING_OPEN;
+ }
+
+ public class CameraUnavailableException extends java.lang.Exception {
+ ctor public CameraUnavailableException(int);
+ ctor public CameraUnavailableException(int, String?);
+ ctor public CameraUnavailableException(int, String?, Throwable?);
+ ctor public CameraUnavailableException(int, Throwable?);
+ method public int getReason();
+ field public static final int CAMERA_DISABLED = 1; // 0x1
+ field public static final int CAMERA_DISCONNECTED = 2; // 0x2
+ field public static final int CAMERA_ERROR = 3; // 0x3
+ field public static final int CAMERA_IN_USE = 4; // 0x4
+ field public static final int CAMERA_MAX_IN_USE = 5; // 0x5
+ field public static final int CAMERA_UNAVAILABLE_DO_NOT_DISTURB = 6; // 0x6
+ field public static final int CAMERA_UNKNOWN_ERROR = 0; // 0x0
+ }
+
+ public final class CameraXConfig {
+ method public androidx.camera.core.CameraSelector? getAvailableCamerasLimiter(androidx.camera.core.CameraSelector?);
+ method public java.util.concurrent.Executor? getCameraExecutor(java.util.concurrent.Executor?);
+ method public long getCameraOpenRetryMaxTimeoutInMillisWhileResuming();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public androidx.camera.core.RetryPolicy getCameraProviderInitRetryPolicy();
+ method public int getMinimumLoggingLevel();
+ method public android.os.Handler? getSchedulerHandler(android.os.Handler?);
+ }
+
+ public static final class CameraXConfig.Builder {
+ method public androidx.camera.core.CameraXConfig build();
+ method public static androidx.camera.core.CameraXConfig.Builder fromConfig(androidx.camera.core.CameraXConfig);
+ method public androidx.camera.core.CameraXConfig.Builder setAvailableCamerasLimiter(androidx.camera.core.CameraSelector);
+ method public androidx.camera.core.CameraXConfig.Builder setCameraExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.CameraXConfig.Builder setCameraOpenRetryMaxTimeoutInMillisWhileResuming(long);
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public androidx.camera.core.CameraXConfig.Builder setCameraProviderInitRetryPolicy(androidx.camera.core.RetryPolicy);
+ method public androidx.camera.core.CameraXConfig.Builder setMinimumLoggingLevel(@IntRange(from=android.util.Log.DEBUG, to=android.util.Log.ERROR) int);
+ method public androidx.camera.core.CameraXConfig.Builder setSchedulerHandler(android.os.Handler);
+ }
+
+ public static interface CameraXConfig.Provider {
+ method public androidx.camera.core.CameraXConfig getCameraXConfig();
+ }
+
+ public class ConcurrentCamera {
+ ctor public ConcurrentCamera(java.util.List<androidx.camera.core.Camera!>);
+ method public java.util.List<androidx.camera.core.Camera!> getCameras();
+ }
+
+ public static final class ConcurrentCamera.SingleCameraConfig {
+ ctor public ConcurrentCamera.SingleCameraConfig(androidx.camera.core.CameraSelector, androidx.camera.core.UseCaseGroup, androidx.lifecycle.LifecycleOwner);
+ method public androidx.camera.core.CameraSelector getCameraSelector();
+ method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
+ method public androidx.camera.core.UseCaseGroup getUseCaseGroup();
+ }
+
+ public final class DisplayOrientedMeteringPointFactory extends androidx.camera.core.MeteringPointFactory {
+ ctor public DisplayOrientedMeteringPointFactory(android.view.Display, androidx.camera.core.CameraInfo, float, float);
+ }
+
+ public final class DynamicRange {
+ ctor public DynamicRange(int, int);
+ method public int getBitDepth();
+ method public int getEncoding();
+ field public static final int BIT_DEPTH_10_BIT = 10; // 0xa
+ field public static final int BIT_DEPTH_8_BIT = 8; // 0x8
+ field public static final int BIT_DEPTH_UNSPECIFIED = 0; // 0x0
+ field public static final androidx.camera.core.DynamicRange DOLBY_VISION_10_BIT;
+ field public static final androidx.camera.core.DynamicRange DOLBY_VISION_8_BIT;
+ field public static final int ENCODING_DOLBY_VISION = 6; // 0x6
+ field public static final int ENCODING_HDR10 = 4; // 0x4
+ field public static final int ENCODING_HDR10_PLUS = 5; // 0x5
+ field public static final int ENCODING_HDR_UNSPECIFIED = 2; // 0x2
+ field public static final int ENCODING_HLG = 3; // 0x3
+ field public static final int ENCODING_SDR = 1; // 0x1
+ field public static final int ENCODING_UNSPECIFIED = 0; // 0x0
+ field public static final androidx.camera.core.DynamicRange HDR10_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HDR10_PLUS_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HDR_UNSPECIFIED_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HLG_10_BIT;
+ field public static final androidx.camera.core.DynamicRange SDR;
+ field public static final androidx.camera.core.DynamicRange UNSPECIFIED;
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraInfo {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalImageCaptureOutputFormat {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLensFacing {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalMirrorMode {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalRetryPolicy {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseApi {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalZeroShutterLag {
+ }
+
+ public interface ExposureState {
+ method public int getExposureCompensationIndex();
+ method public android.util.Range<java.lang.Integer!> getExposureCompensationRange();
+ method public android.util.Rational getExposureCompensationStep();
+ method public boolean isExposureCompensationSupported();
+ }
+
+ public interface ExtendableBuilder<T> {
+ method public T build();
+ }
+
+ public final class FocusMeteringAction {
+ method public long getAutoCancelDurationInMillis();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAe();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAf();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAwb();
+ method public boolean isAutoCancelEnabled();
+ field public static final int FLAG_AE = 2; // 0x2
+ field public static final int FLAG_AF = 1; // 0x1
+ field public static final int FLAG_AWB = 4; // 0x4
+ }
+
+ public static class FocusMeteringAction.Builder {
+ ctor public FocusMeteringAction.Builder(androidx.camera.core.MeteringPoint);
+ ctor public FocusMeteringAction.Builder(androidx.camera.core.MeteringPoint, int);
+ method public androidx.camera.core.FocusMeteringAction.Builder addPoint(androidx.camera.core.MeteringPoint);
+ method public androidx.camera.core.FocusMeteringAction.Builder addPoint(androidx.camera.core.MeteringPoint, int);
+ method public androidx.camera.core.FocusMeteringAction build();
+ method public androidx.camera.core.FocusMeteringAction.Builder disableAutoCancel();
+ method public androidx.camera.core.FocusMeteringAction.Builder setAutoCancelDuration(@IntRange(from=1) long, java.util.concurrent.TimeUnit);
+ }
+
+ public final class FocusMeteringResult {
+ method public boolean isFocusSuccessful();
+ }
+
+ public final class ImageAnalysis extends androidx.camera.core.UseCase {
+ method public void clearAnalyzer();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalUseCaseApi public java.util.concurrent.Executor? getBackgroundExecutor();
+ method public int getBackpressureStrategy();
+ method public int getImageQueueDepth();
+ method public int getOutputImageFormat();
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public int getTargetRotation();
+ method public boolean isOutputImageRotationEnabled();
+ method public void setAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+ method public void setTargetRotation(int);
+ field public static final int COORDINATE_SYSTEM_ORIGINAL = 0; // 0x0
+ field public static final int COORDINATE_SYSTEM_SENSOR = 2; // 0x2
+ field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
+ field public static final int OUTPUT_IMAGE_FORMAT_RGBA_8888 = 2; // 0x2
+ field public static final int OUTPUT_IMAGE_FORMAT_YUV_420_888 = 1; // 0x1
+ field public static final int STRATEGY_BLOCK_PRODUCER = 1; // 0x1
+ field public static final int STRATEGY_KEEP_ONLY_LATEST = 0; // 0x0
+ }
+
+ public static interface ImageAnalysis.Analyzer {
+ method public void analyze(androidx.camera.core.ImageProxy);
+ method public default android.util.Size? getDefaultTargetResolution();
+ method public default int getTargetCoordinateSystem();
+ method public default void updateTransform(android.graphics.Matrix?);
+ }
+
+ public static final class ImageAnalysis.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageAnalysis!> {
+ ctor public ImageAnalysis.Builder();
+ method public androidx.camera.core.ImageAnalysis build();
+ method public androidx.camera.core.ImageAnalysis.Builder setBackgroundExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.ImageAnalysis.Builder setBackpressureStrategy(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setImageQueueDepth(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setOutputImageFormat(int);
+ method @RequiresApi(23) public androidx.camera.core.ImageAnalysis.Builder setOutputImageRotationEnabled(boolean);
+ method public androidx.camera.core.ImageAnalysis.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method @Deprecated public androidx.camera.core.ImageAnalysis.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.ImageAnalysis.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.ImageAnalysis.Builder setTargetRotation(int);
+ }
+
+ public final class ImageCapture extends androidx.camera.core.UseCase {
+ method public int getCaptureMode();
+ method public int getFlashMode();
+ method public static androidx.camera.core.ImageCaptureCapabilities getImageCaptureCapabilities(androidx.camera.core.CameraInfo);
+ method @IntRange(from=1, to=100) public int getJpegQuality();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public int getOutputFormat();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getPostviewResolutionSelector();
+ method public androidx.camera.core.ImageCaptureLatencyEstimate getRealtimeCaptureLatencyEstimate();
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public androidx.camera.core.ImageCapture.ScreenFlash? getScreenFlash();
+ method public int getTargetRotation();
+ method public boolean isPostviewEnabled();
+ method public void setCropAspectRatio(android.util.Rational);
+ method public void setFlashMode(int);
+ method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
+ method public void setTargetRotation(int);
+ method public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
+ method public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
+ field public static final int CAPTURE_MODE_MAXIMIZE_QUALITY = 0; // 0x0
+ field public static final int CAPTURE_MODE_MINIMIZE_LATENCY = 1; // 0x1
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public static final int CAPTURE_MODE_ZERO_SHUTTER_LAG = 2; // 0x2
+ field public static final int ERROR_CAMERA_CLOSED = 3; // 0x3
+ field public static final int ERROR_CAPTURE_FAILED = 2; // 0x2
+ field public static final int ERROR_FILE_IO = 1; // 0x1
+ field public static final int ERROR_INVALID_CAMERA = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 0; // 0x0
+ field public static final int FLASH_MODE_AUTO = 0; // 0x0
+ field public static final int FLASH_MODE_OFF = 2; // 0x2
+ field public static final int FLASH_MODE_ON = 1; // 0x1
+ field public static final int FLASH_MODE_SCREEN = 3; // 0x3
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG = 0; // 0x0
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG_ULTRA_HDR = 1; // 0x1
+ }
+
+ public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture!> {
+ ctor public ImageCapture.Builder();
+ method public androidx.camera.core.ImageCapture build();
+ method public androidx.camera.core.ImageCapture.Builder setCaptureMode(int);
+ method public androidx.camera.core.ImageCapture.Builder setFlashMode(int);
+ method public androidx.camera.core.ImageCapture.Builder setIoExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.ImageCapture.Builder setJpegQuality(@IntRange(from=1, to=100) int);
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public androidx.camera.core.ImageCapture.Builder setOutputFormat(int);
+ method public androidx.camera.core.ImageCapture.Builder setPostviewEnabled(boolean);
+ method public androidx.camera.core.ImageCapture.Builder setPostviewResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method public androidx.camera.core.ImageCapture.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method public androidx.camera.core.ImageCapture.Builder setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash);
+ method @Deprecated public androidx.camera.core.ImageCapture.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.ImageCapture.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.ImageCapture.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.ImageCapture.Builder setTargetRotation(int);
+ }
+
+ public static final class ImageCapture.Metadata {
+ ctor public ImageCapture.Metadata();
+ method public android.location.Location? getLocation();
+ method public boolean isReversedHorizontal();
+ method public boolean isReversedVertical();
+ method public void setLocation(android.location.Location?);
+ method public void setReversedHorizontal(boolean);
+ method public void setReversedVertical(boolean);
+ }
+
+ public abstract static class ImageCapture.OnImageCapturedCallback {
+ ctor public ImageCapture.OnImageCapturedCallback();
+ method public void onCaptureProcessProgressed(int);
+ method public void onCaptureStarted();
+ method public void onCaptureSuccess(androidx.camera.core.ImageProxy);
+ method public void onError(androidx.camera.core.ImageCaptureException);
+ method public void onPostviewBitmapAvailable(android.graphics.Bitmap);
+ }
+
+ public static interface ImageCapture.OnImageSavedCallback {
+ method public default void onCaptureProcessProgressed(int);
+ method public default void onCaptureStarted();
+ method public void onError(androidx.camera.core.ImageCaptureException);
+ method public void onImageSaved(androidx.camera.core.ImageCapture.OutputFileResults);
+ method public default void onPostviewBitmapAvailable(android.graphics.Bitmap);
+ }
+
+ public static final class ImageCapture.OutputFileOptions {
+ }
+
+ public static final class ImageCapture.OutputFileOptions.Builder {
+ ctor public ImageCapture.OutputFileOptions.Builder(android.content.ContentResolver, android.net.Uri, android.content.ContentValues);
+ ctor public ImageCapture.OutputFileOptions.Builder(java.io.File);
+ ctor public ImageCapture.OutputFileOptions.Builder(java.io.OutputStream);
+ method public androidx.camera.core.ImageCapture.OutputFileOptions build();
+ method public androidx.camera.core.ImageCapture.OutputFileOptions.Builder setMetadata(androidx.camera.core.ImageCapture.Metadata);
+ }
+
+ public static class ImageCapture.OutputFileResults {
+ method public android.net.Uri? getSavedUri();
+ }
+
+ public static interface ImageCapture.ScreenFlash {
+ method @UiThread public void apply(long, androidx.camera.core.ImageCapture.ScreenFlashListener);
+ method @UiThread public void clear();
+ }
+
+ public static interface ImageCapture.ScreenFlashListener {
+ method public void onCompleted();
+ }
+
+ public interface ImageCaptureCapabilities {
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public java.util.Set<java.lang.Integer!> getSupportedOutputFormats();
+ method public boolean isCaptureProcessProgressSupported();
+ method public boolean isPostviewSupported();
+ }
+
+ public class ImageCaptureException extends java.lang.Exception {
+ ctor public ImageCaptureException(int, String, Throwable?);
+ method public int getImageCaptureError();
+ }
+
+ public final class ImageCaptureExtKt {
+ method public static suspend Object? takePicture(androidx.camera.core.ImageCapture, androidx.camera.core.ImageCapture.OutputFileOptions outputFileOptions, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onCaptureStarted, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit>? onCaptureProcessProgressed, optional kotlin.jvm.functions.Function1<? super android.graphics.Bitmap,kotlin.Unit>? onPostviewBitmapAvailable, kotlin.coroutines.Continuation<? super androidx.camera.core.ImageCapture.OutputFileResults>);
+ method public static suspend Object? takePicture(androidx.camera.core.ImageCapture, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onCaptureStarted, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit>? onCaptureProcessProgressed, optional kotlin.jvm.functions.Function1<? super android.graphics.Bitmap,kotlin.Unit>? onPostviewBitmapAvailable, kotlin.coroutines.Continuation<? super androidx.camera.core.ImageProxy>);
+ }
+
+ public class ImageCaptureLatencyEstimate {
+ ctor public ImageCaptureLatencyEstimate(long, long);
+ method public long getCaptureLatencyMillis();
+ method public long getProcessingLatencyMillis();
+ method public long getTotalCaptureLatencyMillis();
+ field public static final long UNDEFINED_CAPTURE_LATENCY = -1L; // 0xffffffffffffffffL
+ field public static final androidx.camera.core.ImageCaptureLatencyEstimate UNDEFINED_IMAGE_CAPTURE_LATENCY;
+ field public static final long UNDEFINED_PROCESSING_LATENCY = -1L; // 0xffffffffffffffffL
+ }
+
+ public interface ImageInfo {
+ method public int getRotationDegrees();
+ method public default android.graphics.Matrix getSensorToBufferTransformMatrix();
+ method public long getTimestamp();
+ }
+
+ public interface ImageProcessor {
+ method public androidx.camera.core.ImageProcessor.Response process(androidx.camera.core.ImageProcessor.Request) throws androidx.camera.core.ProcessingException;
+ }
+
+ public static interface ImageProcessor.Request {
+ method public androidx.camera.core.ImageProxy getInputImage();
+ method public int getOutputFormat();
+ }
+
+ public static interface ImageProcessor.Response {
+ method public androidx.camera.core.ImageProxy getOutputImage();
+ }
+
+ public interface ImageProxy extends java.lang.AutoCloseable {
+ method public void close();
+ method public android.graphics.Rect getCropRect();
+ method public int getFormat();
+ method public int getHeight();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalGetImage public android.media.Image? getImage();
+ method public androidx.camera.core.ImageInfo getImageInfo();
+ method public androidx.camera.core.ImageProxy.PlaneProxy![] getPlanes();
+ method public int getWidth();
+ method public void setCropRect(android.graphics.Rect?);
+ method public default android.graphics.Bitmap toBitmap();
+ }
+
+ public static interface ImageProxy.PlaneProxy {
+ method public java.nio.ByteBuffer getBuffer();
+ method public int getPixelStride();
+ method public int getRowStride();
+ }
+
+ public class InitializationException extends java.lang.Exception {
+ ctor public InitializationException(String?);
+ ctor public InitializationException(String?, Throwable?);
+ ctor public InitializationException(Throwable?);
+ }
+
+ public class MeteringPoint {
+ method public float getSize();
+ }
+
+ public abstract class MeteringPointFactory {
+ method public final androidx.camera.core.MeteringPoint createPoint(float, float);
+ method public final androidx.camera.core.MeteringPoint createPoint(float, float, float);
+ method public static float getDefaultPointSize();
+ }
+
+ public class MirrorMode {
+ field public static final int MIRROR_MODE_OFF = 0; // 0x0
+ field public static final int MIRROR_MODE_ON = 1; // 0x1
+ field public static final int MIRROR_MODE_ON_FRONT_ONLY = 2; // 0x2
+ }
+
+ public final class Preview extends androidx.camera.core.UseCase {
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public static androidx.camera.core.PreviewCapabilities getPreviewCapabilities(androidx.camera.core.CameraInfo);
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public android.util.Range<java.lang.Integer!> getTargetFrameRate();
+ method public int getTargetRotation();
+ method public boolean isPreviewStabilizationEnabled();
+ method @UiThread public void setSurfaceProvider(androidx.camera.core.Preview.SurfaceProvider?);
+ method @UiThread public void setSurfaceProvider(java.util.concurrent.Executor, androidx.camera.core.Preview.SurfaceProvider?);
+ method public void setTargetRotation(int);
+ }
+
+ public static final class Preview.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.Preview!> {
+ ctor public Preview.Builder();
+ method public androidx.camera.core.Preview build();
+ method public androidx.camera.core.Preview.Builder setDynamicRange(androidx.camera.core.DynamicRange);
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalMirrorMode public androidx.camera.core.Preview.Builder setMirrorMode(int);
+ method public androidx.camera.core.Preview.Builder setPreviewStabilizationEnabled(boolean);
+ method public androidx.camera.core.Preview.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method @Deprecated public androidx.camera.core.Preview.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.Preview.Builder setTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method public androidx.camera.core.Preview.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.Preview.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.Preview.Builder setTargetRotation(int);
+ }
+
+ public static interface Preview.SurfaceProvider {
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ }
+
+ public interface PreviewCapabilities {
+ method public boolean isStabilizationSupported();
+ }
+
+ public class ProcessingException extends java.lang.Exception {
+ ctor public ProcessingException();
+ }
+
+ public class ResolutionInfo {
+ ctor public ResolutionInfo(android.util.Size, android.graphics.Rect, int);
+ method public android.graphics.Rect getCropRect();
+ method public android.util.Size getResolution();
+ method public int getRotationDegrees();
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public interface RetryPolicy {
+ method public static long getDefaultRetryTimeoutInMillis();
+ method public default long getTimeoutInMillis();
+ method public androidx.camera.core.RetryPolicy.RetryConfig onRetryDecisionRequested(androidx.camera.core.RetryPolicy.ExecutionState);
+ field public static final androidx.camera.core.RetryPolicy DEFAULT;
+ field public static final androidx.camera.core.RetryPolicy NEVER;
+ field public static final androidx.camera.core.RetryPolicy RETRY_UNAVAILABLE_CAMERA;
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.Builder {
+ ctor public RetryPolicy.Builder(androidx.camera.core.RetryPolicy);
+ method public androidx.camera.core.RetryPolicy build();
+ method public androidx.camera.core.RetryPolicy.Builder setTimeoutInMillis(long);
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static interface RetryPolicy.ExecutionState {
+ method public Throwable? getCause();
+ method public long getExecutedTimeInMillis();
+ method public int getNumOfAttempts();
+ method public int getStatus();
+ field public static final int STATUS_CAMERA_UNAVAILABLE = 2; // 0x2
+ field public static final int STATUS_CONFIGURATION_FAIL = 1; // 0x1
+ field public static final int STATUS_UNKNOWN_ERROR = 0; // 0x0
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig {
+ method public static long getDefaultRetryDelayInMillis();
+ method public long getRetryDelayInMillis();
+ method public boolean shouldRetry();
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig DEFAULT_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig MINI_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig NOT_RETRY;
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig.Builder {
+ ctor public RetryPolicy.RetryConfig.Builder();
+ method public androidx.camera.core.RetryPolicy.RetryConfig build();
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setRetryDelayInMillis(@IntRange(from=100, to=2000) long);
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setShouldRetry(boolean);
+ }
+
+ public class SurfaceOrientedMeteringPointFactory extends androidx.camera.core.MeteringPointFactory {
+ ctor public SurfaceOrientedMeteringPointFactory(float, float);
+ ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
+ }
+
+ public interface SurfaceOutput extends java.io.Closeable {
+ method public void close();
+ method public default android.graphics.Matrix getSensorToBufferTransform();
+ method public android.util.Size getSize();
+ method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+ method public int getTargets();
+ method public void updateTransformMatrix(float[], float[]);
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+ method public abstract int getEventCode();
+ method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+ field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+ }
+
+ public interface SurfaceProcessor {
+ method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+ method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+ }
+
+ public final class SurfaceRequest {
+ method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
+ method public void clearTransformationInfoListener();
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public android.util.Size getResolution();
+ method public boolean invalidate();
+ method public void provideSurface(android.view.Surface, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceRequest.Result!>);
+ method public void setTransformationInfoListener(java.util.concurrent.Executor, androidx.camera.core.SurfaceRequest.TransformationInfoListener);
+ method public boolean willNotProvideSurface();
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceRequest.Result {
+ method public abstract int getResultCode();
+ method public abstract android.view.Surface getSurface();
+ field public static final int RESULT_INVALID_SURFACE = 2; // 0x2
+ field public static final int RESULT_REQUEST_CANCELLED = 1; // 0x1
+ field public static final int RESULT_SURFACE_ALREADY_PROVIDED = 3; // 0x3
+ field public static final int RESULT_SURFACE_USED_SUCCESSFULLY = 0; // 0x0
+ field public static final int RESULT_WILL_NOT_PROVIDE_SURFACE = 4; // 0x4
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceRequest.TransformationInfo {
+ method public abstract android.graphics.Rect getCropRect();
+ method public abstract int getRotationDegrees();
+ method public abstract android.graphics.Matrix getSensorToBufferTransform();
+ method public abstract boolean hasCameraTransform();
+ method public abstract boolean isMirroring();
+ }
+
+ public static interface SurfaceRequest.TransformationInfoListener {
+ method public void onTransformationInfoUpdate(androidx.camera.core.SurfaceRequest.TransformationInfo);
+ }
+
+ public class TorchState {
+ field public static final int OFF = 0; // 0x0
+ field public static final int ON = 1; // 0x1
+ }
+
+ public abstract class UseCase {
+ method public static int snapToSurfaceRotation(@IntRange(from=0, to=359) int);
+ }
+
+ public final class UseCaseGroup {
+ method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
+ method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
+ method public androidx.camera.core.ViewPort? getViewPort();
+ }
+
+ public static final class UseCaseGroup.Builder {
+ ctor public UseCaseGroup.Builder();
+ method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
+ method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
+ method public androidx.camera.core.UseCaseGroup build();
+ method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
+ }
+
+ public final class ViewPort {
+ method public android.util.Rational getAspectRatio();
+ method public int getLayoutDirection();
+ method public int getRotation();
+ method public int getScaleType();
+ field public static final int FILL_CENTER = 1; // 0x1
+ field public static final int FILL_END = 2; // 0x2
+ field public static final int FILL_START = 0; // 0x0
+ field public static final int FIT = 3; // 0x3
+ }
+
+ public static final class ViewPort.Builder {
+ ctor public ViewPort.Builder(android.util.Rational, int);
+ method public androidx.camera.core.ViewPort build();
+ method public androidx.camera.core.ViewPort.Builder setLayoutDirection(int);
+ method public androidx.camera.core.ViewPort.Builder setScaleType(int);
+ }
+
+ public interface ZoomState {
+ method public float getLinearZoom();
+ method public float getMaxZoomRatio();
+ method public float getMinZoomRatio();
+ method public float getZoomRatio();
+ }
+
+}
+
+package androidx.camera.core.resolutionselector {
+
+ public final class AspectRatioStrategy {
+ ctor public AspectRatioStrategy(int, int);
+ method public int getFallbackRule();
+ method public int getPreferredAspectRatio();
+ field public static final int FALLBACK_RULE_AUTO = 1; // 0x1
+ field public static final int FALLBACK_RULE_NONE = 0; // 0x0
+ field public static final androidx.camera.core.resolutionselector.AspectRatioStrategy RATIO_16_9_FALLBACK_AUTO_STRATEGY;
+ field public static final androidx.camera.core.resolutionselector.AspectRatioStrategy RATIO_4_3_FALLBACK_AUTO_STRATEGY;
+ }
+
+ public interface ResolutionFilter {
+ method public java.util.List<android.util.Size!> filter(java.util.List<android.util.Size!>, int);
+ }
+
+ public final class ResolutionSelector {
+ method public int getAllowedResolutionMode();
+ method public androidx.camera.core.resolutionselector.AspectRatioStrategy getAspectRatioStrategy();
+ method public androidx.camera.core.resolutionselector.ResolutionFilter? getResolutionFilter();
+ method public androidx.camera.core.resolutionselector.ResolutionStrategy? getResolutionStrategy();
+ field public static final int PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION = 0; // 0x0
+ field public static final int PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE = 1; // 0x1
+ }
+
+ public static final class ResolutionSelector.Builder {
+ ctor public ResolutionSelector.Builder();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector build();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setAllowedResolutionMode(int);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setAspectRatioStrategy(androidx.camera.core.resolutionselector.AspectRatioStrategy);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setResolutionFilter(androidx.camera.core.resolutionselector.ResolutionFilter);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setResolutionStrategy(androidx.camera.core.resolutionselector.ResolutionStrategy);
+ }
+
+ public final class ResolutionStrategy {
+ ctor public ResolutionStrategy(android.util.Size, int);
+ method public android.util.Size? getBoundSize();
+ method public int getFallbackRule();
+ field public static final int FALLBACK_RULE_CLOSEST_HIGHER = 2; // 0x2
+ field public static final int FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER = 1; // 0x1
+ field public static final int FALLBACK_RULE_CLOSEST_LOWER = 4; // 0x4
+ field public static final int FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER = 3; // 0x3
+ field public static final int FALLBACK_RULE_NONE = 0; // 0x0
+ field public static final androidx.camera.core.resolutionselector.ResolutionStrategy HIGHEST_AVAILABLE_STRATEGY;
+ }
+
+}
+
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
index 80e6ea3..daea541 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
@@ -75,8 +75,10 @@
@Override
@NonNull
- protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
- return suggestedStreamSpec;
+ protected StreamSpec onSuggestedStreamSpecUpdated(
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
+ return primaryStreamSpec;
}
/** Returns true if {@link #onUnbind()} has been called previously. */
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageAnalysisDeviceTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageAnalysisDeviceTest.java
index 2b4f2ff..955e0de 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageAnalysisDeviceTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageAnalysisDeviceTest.java
@@ -49,7 +49,7 @@
ImageAnalysis useCase = new ImageAnalysis.Builder().setBackpressureStrategy(
STRATEGY_KEEP_ONLY_LATEST).build();
- useCase.bindToCamera(mMockCameraInternal, null, null);
+ useCase.bindToCamera(mMockCameraInternal, null, null, null);
useCase.setAnalyzer(CameraXExecutors.mainThreadExecutor(), mMockAnalyzer);
@@ -63,7 +63,7 @@
ImageAnalysis useCase = new ImageAnalysis.Builder().setBackpressureStrategy(
STRATEGY_KEEP_ONLY_LATEST).build();
- useCase.bindToCamera(mMockCameraInternal, null, null);
+ useCase.bindToCamera(mMockCameraInternal, null, null, null);
useCase.setAnalyzer(CameraXExecutors.mainThreadExecutor(), mMockAnalyzer);
useCase.clearAnalyzer();
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index f452482..263b7a5 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -520,6 +520,63 @@
assertThat(resolutionInfo.getCropRect()).isEqualTo(new Rect(0, 60, 640, 420));
}
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ public void streamSpecZslNotDisabled_zslConfigAdded() {
+ ImageCapture imageCapture = new ImageCapture.Builder().setCaptureMode(
+ ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG).build();
+
+ mInstrumentation.runOnMainSync(() -> {
+ try {
+ mCameraUseCaseAdapter.addUseCases(Collections.singletonList(imageCapture));
+ } catch (CameraUseCaseAdapter.CameraException e) {
+ }
+ }
+ );
+
+ FakeCameraControl fakeCameraControl =
+ getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
+
+ assertThat(fakeCameraControl.isZslConfigAdded()).isTrue();
+ }
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ public void streamSpecZslDisabled_zslConfigNotAdded() {
+ FakeCamera fakeCamera = new FakeCamera("fakeCameraId");
+
+ FakeCameraDeviceSurfaceManager fakeCameraDeviceSurfaceManager =
+ new FakeCameraDeviceSurfaceManager();
+ fakeCameraDeviceSurfaceManager.setSuggestedStreamSpec("fakeCameraId",
+ ImageCaptureConfig.class,
+ StreamSpec.builder(new Size(640, 480))
+ .setZslDisabled(true)
+ .build());
+
+ UseCaseConfigFactory useCaseConfigFactory = new FakeUseCaseConfigFactory();
+ mCameraUseCaseAdapter = new CameraUseCaseAdapter(
+ fakeCamera,
+ new FakeCameraCoordinator(),
+ fakeCameraDeviceSurfaceManager,
+ useCaseConfigFactory);
+
+ ImageCapture imageCapture = new ImageCapture.Builder().setCaptureMode(
+ ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG).build();
+
+ mInstrumentation.runOnMainSync(() -> {
+ try {
+ mCameraUseCaseAdapter.addUseCases(Collections.singletonList(imageCapture));
+ } catch (CameraUseCaseAdapter.CameraException e) {
+ }
+ }
+ );
+
+ FakeCameraControl fakeCameraControl =
+ getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
+
+ assertThat(fakeCameraControl.isZslConfigAdded()).isFalse();
+ }
+
private boolean hasJpegQuality(List<CaptureConfig> captureConfigs, int jpegQuality) {
for (CaptureConfig captureConfig : captureConfigs) {
if (jpegQuality == captureConfig.getImplementationOptions().retrieveOption(
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
index 2271b6f..eb99fa1d 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
@@ -89,7 +89,7 @@
fun getAttachedSessionConfig() {
val testUseCase = createFakeUseCase()
val sessionToAttach = SessionConfig.Builder().build()
- testUseCase.updateSessionConfig(sessionToAttach)
+ testUseCase.updateSessionConfig(listOf(sessionToAttach))
val attachedSession = testUseCase.sessionConfig
assertThat(attachedSession).isEqualTo(sessionToAttach)
}
@@ -97,7 +97,7 @@
@Test
fun removeListener() {
val testUseCase = createFakeUseCase()
- testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null, null)
testUseCase.unbindFromCamera(fakeCamera)
testUseCase.notifyActive()
assertThat(fakeCamera.useCaseActiveHistory).isEmpty()
@@ -106,7 +106,7 @@
@Test
fun notifyActiveState() {
val testUseCase = createFakeUseCase()
- testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null, null)
testUseCase.notifyActive()
assertThat(fakeCamera.useCaseActiveHistory[0]).isEqualTo(testUseCase)
}
@@ -114,7 +114,7 @@
@Test
fun notifyInactiveState() {
val testUseCase = createFakeUseCase()
- testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null, null)
testUseCase.notifyInactive()
assertThat(fakeCamera.useCaseInactiveHistory[0]).isEqualTo(testUseCase)
}
@@ -122,7 +122,7 @@
@Test
fun notifyUpdatedSettings() {
val testUseCase = FakeUseCase()
- testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null, null)
testUseCase.notifyUpdated()
assertThat(fakeCamera.useCaseUpdateHistory[0]).isEqualTo(testUseCase)
}
@@ -130,7 +130,7 @@
@Test
fun notifyResetUseCase() {
val testUseCase = FakeUseCase()
- testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null, null)
testUseCase.notifyReset()
assertThat(fakeCamera.useCaseResetHistory[0]).isEqualTo(testUseCase)
}
@@ -149,9 +149,9 @@
@Test
fun attachedSurfaceResolutionCanBeReset_whenOnDetach() {
val testUseCase = FakeUseCase()
- testUseCase.updateSuggestedStreamSpec(TEST_STREAM_SPEC)
+ testUseCase.updateSuggestedStreamSpec(TEST_STREAM_SPEC, null)
assertThat(testUseCase.attachedSurfaceResolution).isNotNull()
- testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null, null)
testUseCase.unbindFromCamera(fakeCamera)
assertThat(testUseCase.attachedSurfaceResolution).isNull()
}
@@ -159,9 +159,9 @@
@Test
fun attachedStreamSpecCanBeReset_whenOnDetach() {
val testUseCase = FakeUseCase()
- testUseCase.updateSuggestedStreamSpec(TEST_STREAM_SPEC)
+ testUseCase.updateSuggestedStreamSpec(TEST_STREAM_SPEC, null)
assertThat(testUseCase.attachedStreamSpec).isNotNull()
- testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null, null)
testUseCase.unbindFromCamera(fakeCamera)
assertThat(testUseCase.attachedStreamSpec).isNull()
}
@@ -171,7 +171,7 @@
val testUseCase = FakeUseCase()
testUseCase.setViewPortCropRect(Rect(0, 0, 640, 480))
assertThat(testUseCase.viewPortCropRect).isNotNull()
- testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null, null)
testUseCase.unbindFromCamera(fakeCamera)
assertThat(testUseCase.viewPortCropRect).isNull()
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index fe1460f..2fe4b2e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -446,7 +446,7 @@
mSessionConfigBuilder = createPipeline(getCameraId(),
(ImageAnalysisConfig) getCurrentConfig(),
Preconditions.checkNotNull(getAttachedStreamSpec()));
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
notifyReset();
});
@@ -785,14 +785,16 @@
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
+ protected StreamSpec onSuggestedStreamSpecUpdated(
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
final ImageAnalysisConfig config = (ImageAnalysisConfig) getCurrentConfig();
mSessionConfigBuilder = createPipeline(getCameraId(), config,
- suggestedStreamSpec);
- updateSessionConfig(mSessionConfigBuilder.build());
+ primaryStreamSpec);
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
- return suggestedStreamSpec;
+ return primaryStreamSpec;
}
/**
@@ -803,7 +805,7 @@
@RestrictTo(Scope.LIBRARY_GROUP)
protected StreamSpec onSuggestedStreamSpecImplementationOptionsUpdated(@NonNull Config config) {
mSessionConfigBuilder.addImplementationOptions(config);
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
return getAttachedStreamSpec().toBuilder().setImplementationOptions(config).build();
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 6ec34f5..5c15ced 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -1188,16 +1188,18 @@
@NonNull
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
- protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
+ protected StreamSpec onSuggestedStreamSpecUpdated(
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
mSessionConfigBuilder = createPipeline(getCameraId(),
- (ImageCaptureConfig) getCurrentConfig(), suggestedStreamSpec);
+ (ImageCaptureConfig) getCurrentConfig(), primaryStreamSpec);
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
// In order to speed up the take picture process, notifyActive at an early stage to
// attach the session capture callback to repeating and get capture result all the time.
notifyActive();
- return suggestedStreamSpec;
+ return primaryStreamSpec;
}
/**
@@ -1208,7 +1210,7 @@
@RestrictTo(Scope.LIBRARY_GROUP)
protected StreamSpec onSuggestedStreamSpecImplementationOptionsUpdated(@NonNull Config config) {
mSessionConfigBuilder.addImplementationOptions(config);
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
return getAttachedStreamSpec().toBuilder().setImplementationOptions(config).build();
}
@@ -1317,7 +1319,9 @@
SessionConfig.Builder sessionConfigBuilder =
mImagePipeline.createSessionConfigBuilder(streamSpec.getResolution());
- if (Build.VERSION.SDK_INT >= 23 && getCaptureMode() == CAPTURE_MODE_ZERO_SHUTTER_LAG) {
+ if (Build.VERSION.SDK_INT >= 23
+ && getCaptureMode() == CAPTURE_MODE_ZERO_SHUTTER_LAG
+ && !streamSpec.getZslDisabled()) {
getCameraControl().addZslConfig(sessionConfigBuilder);
}
if (streamSpec.getImplementationOptions() != null) {
@@ -1339,12 +1343,11 @@
mSessionConfigBuilder = createPipeline(getCameraId(),
(ImageCaptureConfig) getCurrentConfig(),
Preconditions.checkNotNull(getAttachedStreamSpec()));
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
notifyReset();
mTakePictureManager.resume();
});
sessionConfigBuilder.setErrorListener(mCloseableErrorListener);
-
return sessionConfigBuilder;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index d6c425d..a7393f9 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -492,7 +492,7 @@
private void updateConfigAndOutput(@NonNull PreviewConfig config,
@NonNull StreamSpec streamSpec) {
mSessionConfigBuilder = createPipeline(config, streamSpec);
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
}
/**
@@ -609,9 +609,11 @@
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
- updateConfigAndOutput((PreviewConfig) getCurrentConfig(), suggestedStreamSpec);
- return suggestedStreamSpec;
+ protected StreamSpec onSuggestedStreamSpecUpdated(
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
+ updateConfigAndOutput((PreviewConfig) getCurrentConfig(), primaryStreamSpec);
+ return primaryStreamSpec;
}
/**
@@ -622,7 +624,7 @@
@RestrictTo(Scope.LIBRARY_GROUP)
protected StreamSpec onSuggestedStreamSpecImplementationOptionsUpdated(@NonNull Config config) {
mSessionConfigBuilder.addImplementationOptions(config);
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
return getAttachedStreamSpec().toBuilder().setImplementationOptions(config).build();
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index 08554aaa..6706f0f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -66,6 +66,7 @@
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -148,6 +149,10 @@
@GuardedBy("mCameraLock")
private CameraInternal mCamera;
+ @GuardedBy("mCameraLock")
+ @Nullable
+ private CameraInternal mSecondaryCamera;
+
@Nullable
private CameraEffect mEffect;
@@ -162,6 +167,11 @@
@NonNull
private SessionConfig mAttachedSessionConfig = SessionConfig.defaultEmptySessionConfig();
+ // The currently attached session config for secondary camera in dual camera case
+ @NonNull
+ private SessionConfig mAttachedSecondarySessionConfig =
+ SessionConfig.defaultEmptySessionConfig();
+
/**
* Creates a named instance of the use case.
*
@@ -512,11 +522,21 @@
*
*/
@RestrictTo(Scope.LIBRARY_GROUP)
- protected void updateSessionConfig(@NonNull SessionConfig sessionConfig) {
- mAttachedSessionConfig = sessionConfig;
- for (DeferrableSurface surface : sessionConfig.getSurfaces()) {
- if (surface.getContainerClass() == null) {
- surface.setContainerClass(this.getClass());
+ protected void updateSessionConfig(@NonNull List<SessionConfig> sessionConfigs) {
+ if (sessionConfigs.isEmpty()) {
+ return;
+ }
+
+ mAttachedSessionConfig = sessionConfigs.get(0);
+ if (sessionConfigs.size() > 1) {
+ mAttachedSecondarySessionConfig = sessionConfigs.get(1);
+ }
+
+ for (SessionConfig sessionConfig : sessionConfigs) {
+ for (DeferrableSurface surface : sessionConfig.getSurfaces()) {
+ if (surface.getContainerClass() == null) {
+ surface.setContainerClass(this.getClass());
+ }
}
}
}
@@ -550,6 +570,16 @@
}
/**
+ * Get the current {@link SessionConfig} of the secondary camera in dual camera case.
+ *
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public SessionConfig getSecondarySessionConfig() {
+ return mAttachedSecondarySessionConfig;
+ }
+
+ /**
* Notify all {@link StateChangeCallback} that are listening to this UseCase that it has
* transitioned to an active state.
*
@@ -629,6 +659,18 @@
}
/**
+ * Returns the camera ID for the currently attached secondary camera, or throws an exception if
+ * no camera is attached.
+ *
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ protected String getSecondaryCameraId() {
+ return getSecondaryCamera() == null ? null : getSecondaryCamera()
+ .getCameraInfoInternal().getCameraId();
+ }
+
+ /**
* Checks whether the provided camera ID is the currently attached camera ID.
*
*/
@@ -680,6 +722,18 @@
}
/**
+ * Returns the currently attached secondary {@link Camera} or {@code null} if none is attached.
+ *
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public CameraInternal getSecondaryCamera() {
+ synchronized (mCameraLock) {
+ return mSecondaryCamera;
+ }
+ }
+
+ /**
* Retrieves the currently attached surface resolution.
*
* @return the currently attached surface resolution for the given camera id.
@@ -706,26 +760,33 @@
*
*/
@RestrictTo(Scope.LIBRARY_GROUP)
- public void updateSuggestedStreamSpec(@NonNull StreamSpec suggestedStreamSpec) {
- mAttachedStreamSpec = onSuggestedStreamSpecUpdated(suggestedStreamSpec);
+ public void updateSuggestedStreamSpec(
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
+ mAttachedStreamSpec = onSuggestedStreamSpecUpdated(
+ primaryStreamSpec, secondaryStreamSpec);
}
/**
* Called when binding new use cases via {@code CameraX#bindToLifecycle(LifecycleOwner,
- * CameraSelector, UseCase...)}.
+ * CameraSelector, UseCase...)} with additional information for dual cameras.
*
* <p>Override to create necessary objects like {@link ImageReader} depending
* on the stream specification.
*
- * @param suggestedStreamSpec The suggested stream specification that depends on camera device
+ * @param primaryStreamSpec The suggested stream specification that depends on camera device
* capability and what and how many use cases will be bound.
+ * @param secondaryStreamSpec The suggested stream specification for secondary camera in
+ * dual camera case.
* @return The stream specification that finally used to create the SessionConfig to
* attach to the camera device.
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
- return suggestedStreamSpec;
+ protected StreamSpec onSuggestedStreamSpecUpdated(
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
+ return primaryStreamSpec;
}
/**
@@ -734,6 +795,8 @@
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public void updateSuggestedStreamSpecImplementationOptions(@NonNull Config config) {
+ // TODO(b/349823704): investigate whether we need mAttachedSecondaryStreamSpec for
+ // StreamSharing
mAttachedStreamSpec = onSuggestedStreamSpecImplementationOptionsUpdated(config);
}
@@ -770,9 +833,9 @@
* <p>Before a use case can receive frame data, it needs to establish association with the
* target camera first. An implementation of {@link CameraInternal} (e.g. a lifecycle camera
* or lifecycle-less camera) is provided when
- * {@link #bindToCamera(CameraInternal, UseCaseConfig, UseCaseConfig)} is invoked, so that the
- * use case can retrieve the necessary information from the camera to calculate and set up
- * the configs.
+ * {@link #bindToCamera(CameraInternal, CameraInternal, UseCaseConfig, UseCaseConfig)} is
+ * invoked, so that the use case can retrieve the necessary information from the camera
+ * to calculate and set up the configs.
*
* <p>The default, extended and camera config settings are also applied to the use case config
* in this stage. Subclasses can override {@link #onMergeConfig} to update the use case
@@ -786,11 +849,16 @@
@SuppressLint("WrongConstant")
@RestrictTo(Scope.LIBRARY_GROUP)
public final void bindToCamera(@NonNull CameraInternal camera,
+ @Nullable CameraInternal secondaryCamera,
@Nullable UseCaseConfig<?> extendedConfig,
@Nullable UseCaseConfig<?> cameraConfig) {
synchronized (mCameraLock) {
mCamera = camera;
+ mSecondaryCamera = secondaryCamera;
addStateChangeCallback(camera);
+ if (secondaryCamera != null) {
+ addStateChangeCallback(secondaryCamera);
+ }
}
mExtendedConfig = extendedConfig;
@@ -835,9 +903,17 @@
onUnbind();
synchronized (mCameraLock) {
- checkArgument(camera == mCamera);
- removeStateChangeCallback(mCamera);
- mCamera = null;
+ checkArgument(camera == mCamera || camera == mSecondaryCamera);
+
+ if (camera == mCamera) {
+ removeStateChangeCallback(mCamera);
+ mCamera = null;
+ }
+
+ if (camera == mSecondaryCamera) {
+ removeStateChangeCallback(mSecondaryCamera);
+ mSecondaryCamera = null;
+ }
}
mAttachedStreamSpec = null;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
index ceed873..5788082 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
@@ -82,6 +82,7 @@
* supported output sizes list that will be given a
* suggested stream specification
* @param isPreviewStabilizationOn whether the preview stabilization is enabled.
+ * @param hasVideoCapture whether the use cases has video capture.
* @return map of suggested stream specifications for given use cases
* @throws IllegalStateException if not initialized
* @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
@@ -96,5 +97,6 @@
@NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
@NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap,
- boolean isPreviewStabilizationOn);
+ boolean isPreviewStabilizationOn,
+ boolean hasVideoCapture);
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/StreamSpec.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/StreamSpec.java
index 62f0c09..e6eab05 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/StreamSpec.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/StreamSpec.java
@@ -68,13 +68,19 @@
@Nullable
public abstract Config getImplementationOptions();
+ /**
+ * Returns the flag if zero-shutter lag needs to be disabled by user case combinations.
+ */
+ public abstract boolean getZslDisabled();
+
/** Returns a build for a stream configuration that takes a required resolution. */
@NonNull
public static Builder builder(@NonNull Size resolution) {
return new AutoValue_StreamSpec.Builder()
.setResolution(resolution)
.setExpectedFrameRateRange(FRAME_RATE_RANGE_UNSPECIFIED)
- .setDynamicRange(DynamicRange.SDR);
+ .setDynamicRange(DynamicRange.SDR)
+ .setZslDisabled(false);
}
/** Returns a builder pre-populated with the current specification. */
@@ -118,6 +124,12 @@
@NonNull
public abstract Builder setImplementationOptions(@NonNull Config config);
+ /**
+ * Sets the flag if zero-shutter lag needs to be disabled by user case combinations.
+ */
+ @NonNull
+ public abstract Builder setZslDisabled(boolean disabled);
+
/** Builds the stream specification */
@NonNull
public abstract StreamSpec build();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index 4203efc..caa082e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -57,6 +57,7 @@
import androidx.camera.core.CameraSelector;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.ImageCapture;
+import androidx.camera.core.LayoutSettings;
import androidx.camera.core.Logger;
import androidx.camera.core.Preview;
import androidx.camera.core.UseCase;
@@ -93,6 +94,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
@@ -108,6 +110,8 @@
public final class CameraUseCaseAdapter implements Camera {
@NonNull
private final CameraInternal mCameraInternal;
+ @Nullable
+ private final CameraInternal mSecondaryCameraInternal;
private final CameraDeviceSurfaceManager mCameraDeviceSurfaceManager;
private final UseCaseConfigFactory mUseCaseConfigFactory;
@@ -164,6 +168,12 @@
private final RestrictedCameraControl mAdapterCameraControl;
@NonNull
private final RestrictedCameraInfo mAdapterCameraInfo;
+
+ @NonNull
+ private final LayoutSettings mLayoutSettings;
+ @NonNull
+ private final LayoutSettings mSecondaryLayoutSettings;
+
/**
* Create a new {@link CameraUseCaseAdapter} instance.
*
@@ -179,8 +189,11 @@
@NonNull CameraDeviceSurfaceManager cameraDeviceSurfaceManager,
@NonNull UseCaseConfigFactory useCaseConfigFactory) {
this(camera,
+ null,
new RestrictedCameraInfo(camera.getCameraInfoInternal(),
CameraConfigs.defaultConfig()),
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
cameraCoordinator,
cameraDeviceSurfaceManager,
useCaseConfigFactory);
@@ -190,9 +203,13 @@
* Create a new {@link CameraUseCaseAdapter} instance.
*
* @param camera The camera that is wrapped.
+ * @param secondaryCamera The secondary camera that is wrapped in dual camera case.
* @param restrictedCameraInfo The {@link RestrictedCameraInfo} that contains the extra
* information to configure the {@link CameraInternal} when
* attaching the uses cases of this adapter to the camera.
+ * @param layoutSettings The {@link LayoutSettings} of the camera in dual camera.
+ * @param secondaryLayoutSettings The {@link LayoutSettings} of the secondary camera
+ * in dual camera.
* @param cameraCoordinator Camera coordinator that exposes concurrent camera mode.
* @param cameraDeviceSurfaceManager A class that checks for whether a specific camera
* can support the set of Surface with set resolutions.
@@ -200,11 +217,17 @@
* each UseCase.
*/
public CameraUseCaseAdapter(@NonNull CameraInternal camera,
+ @Nullable CameraInternal secondaryCamera,
@NonNull RestrictedCameraInfo restrictedCameraInfo,
+ @NonNull LayoutSettings layoutSettings,
+ @NonNull LayoutSettings secondaryLayoutSettings,
@NonNull CameraCoordinator cameraCoordinator,
@NonNull CameraDeviceSurfaceManager cameraDeviceSurfaceManager,
@NonNull UseCaseConfigFactory useCaseConfigFactory) {
mCameraInternal = camera;
+ mSecondaryCameraInternal = secondaryCamera;
+ mLayoutSettings = layoutSettings;
+ mSecondaryLayoutSettings = secondaryLayoutSettings;
mCameraCoordinator = cameraCoordinator;
mCameraDeviceSurfaceManager = cameraDeviceSurfaceManager;
mUseCaseConfigFactory = useCaseConfigFactory;
@@ -260,7 +283,7 @@
}
/**
- * Add the specified collection of {@link UseCase} to the adapter.
+ * Add the specified collection of {@link UseCase} to the adapter with dual camera support.
*
* @throws CameraException Thrown if the combination of newly added UseCases and the
* currently added UseCases exceed the capability of the camera.
@@ -269,12 +292,16 @@
synchronized (mLock) {
// Configure the CameraConfig when binding
mCameraInternal.setExtendedConfig(mCameraConfig);
+ if (mSecondaryCameraInternal != null) {
+ mSecondaryCameraInternal.setExtendedConfig(mCameraConfig);
+ }
Set<UseCase> appUseCases = new LinkedHashSet<>(mAppUseCases);
//TODO(b/266641900): must be LinkedHashSet otherwise ExistingActivityLifecycleTest
// fails due to a camera-pipe integration bug.
appUseCases.addAll(appUseCasesToAdd);
try {
- updateUseCases(appUseCases);
+ updateUseCases(appUseCases,
+ mSecondaryCameraInternal != null, mSecondaryCameraInternal != null);
} catch (IllegalArgumentException e) {
throw new CameraException(e);
}
@@ -288,7 +315,8 @@
synchronized (mLock) {
Set<UseCase> appUseCases = new LinkedHashSet<>(mAppUseCases);
appUseCases.removeAll(useCasesToRemove);
- updateUseCases(appUseCases);
+ updateUseCases(appUseCases,
+ mSecondaryCameraInternal != null, mSecondaryCameraInternal != null);
}
}
@@ -296,7 +324,7 @@
* Updates the states based the new app UseCases.
*/
void updateUseCases(@NonNull Collection<UseCase> appUseCases) {
- updateUseCases(appUseCases, /*applyStreamSharing*/false);
+ updateUseCases(appUseCases, /*applyStreamSharing*/false, /*isDualCamera*/false);
}
/**
@@ -309,7 +337,9 @@
* @throws IllegalArgumentException if the UseCase combination is not supported. In that case,
* it will not update the internal states.
*/
- void updateUseCases(@NonNull Collection<UseCase> appUseCases, boolean applyStreamSharing) {
+ void updateUseCases(@NonNull Collection<UseCase> appUseCases,
+ boolean applyStreamSharing,
+ boolean isDualCamera) {
synchronized (mLock) {
checkUnsupportedFeatureCombinationAndThrow(appUseCases);
@@ -317,7 +347,7 @@
// applyStreamSharing is set to true when the use case combination contains
// VideoCapture and Extensions is enabled.
if (!applyStreamSharing && hasExtension() && hasVideoCapture(appUseCases)) {
- updateUseCases(appUseCases, /*applyStreamSharing*/true);
+ updateUseCases(appUseCases, /*applyStreamSharing*/true, isDualCamera);
return;
}
@@ -343,12 +373,20 @@
Map<UseCase, ConfigPair> configs = getConfigs(cameraUseCasesToAttach,
mCameraConfig.getUseCaseConfigFactory(), mUseCaseConfigFactory);
- Map<UseCase, StreamSpec> suggestedStreamSpecMap;
+ Map<UseCase, StreamSpec> primaryStreamSpecMap;
+ Map<UseCase, StreamSpec> secondaryStreamSpecMap = Collections.emptyMap();
try {
- suggestedStreamSpecMap = calculateSuggestedStreamSpecs(
+ primaryStreamSpecMap = calculateSuggestedStreamSpecs(
getCameraMode(),
mCameraInternal.getCameraInfoInternal(), cameraUseCasesToAttach,
cameraUseCasesToKeep, configs);
+ if (mSecondaryCameraInternal != null) {
+ secondaryStreamSpecMap = calculateSuggestedStreamSpecs(
+ getCameraMode(),
+ requireNonNull(mSecondaryCameraInternal).getCameraInfoInternal(),
+ cameraUseCasesToAttach,
+ cameraUseCasesToKeep, configs);
+ }
// TODO(b/265704882): enable stream sharing for LEVEL_3 and high preview
// resolution. Throw exception here if (applyStreamSharing == false), both video
// and preview are used and preview resolution is lower than user configuration.
@@ -362,7 +400,7 @@
&& mCameraCoordinator.getCameraOperatingMode()
!= CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT) {
// Try again and see if StreamSharing resolves the issue.
- updateUseCases(appUseCases, /*applyStreamSharing*/true);
+ updateUseCases(appUseCases, /*applyStreamSharing*/true, isDualCamera);
return;
} else {
// If StreamSharing already on or not enabled, throw exception.
@@ -371,7 +409,7 @@
}
// Update properties.
- updateViewPort(suggestedStreamSpecMap, cameraUseCases);
+ updateViewPort(primaryStreamSpecMap, cameraUseCases);
updateEffects(mEffects, cameraUseCases, appUseCases);
// Detach unused UseCases.
@@ -380,13 +418,23 @@
}
mCameraInternal.detachUseCases(cameraUseCasesToDetach);
+ // Detach unused UseCases for secondary camera.
+ if (mSecondaryCameraInternal != null) {
+ for (UseCase useCase : cameraUseCasesToDetach) {
+ useCase.unbindFromCamera(requireNonNull(mSecondaryCameraInternal));
+ }
+ requireNonNull(mSecondaryCameraInternal)
+ .detachUseCases(cameraUseCasesToDetach);
+ }
+
// Update StreamSpec for UseCases to keep.
if (cameraUseCasesToDetach.isEmpty()) {
// Only do this if we are not removing UseCase, because updating SessionConfig
// when removing UseCases may lead to flickering.
for (UseCase useCase : cameraUseCasesToKeep) {
- if (suggestedStreamSpecMap.containsKey(useCase)) {
- StreamSpec newStreamSpec = suggestedStreamSpecMap.get(useCase);
+ // Assume secondary camera will not have implementation options in dual camera.
+ if (primaryStreamSpecMap.containsKey(useCase)) {
+ StreamSpec newStreamSpec = primaryStreamSpecMap.get(useCase);
Config config = newStreamSpec.getImplementationOptions();
if (config != null && hasImplementationOptionChanged(newStreamSpec,
useCase.getSessionConfig())) {
@@ -394,6 +442,10 @@
}
if (mAttached) {
mCameraInternal.onUseCaseUpdated(useCase);
+ if (mSecondaryCameraInternal != null) {
+ requireNonNull(mSecondaryCameraInternal)
+ .onUseCaseUpdated(useCase);
+ }
}
}
}
@@ -402,13 +454,30 @@
// Attach new UseCases.
for (UseCase useCase : cameraUseCasesToAttach) {
ConfigPair configPair = requireNonNull(configs.get(useCase));
- useCase.bindToCamera(mCameraInternal, configPair.mExtendedConfig,
- configPair.mCameraConfig);
- useCase.updateSuggestedStreamSpec(
- Preconditions.checkNotNull(suggestedStreamSpecMap.get(useCase)));
+ if (mSecondaryCameraInternal != null) {
+ useCase.bindToCamera(mCameraInternal,
+ requireNonNull(mSecondaryCameraInternal),
+ configPair.mExtendedConfig,
+ configPair.mCameraConfig);
+ useCase.updateSuggestedStreamSpec(
+ Preconditions.checkNotNull(primaryStreamSpecMap.get(useCase)),
+ secondaryStreamSpecMap.get(useCase));
+ } else {
+ useCase.bindToCamera(mCameraInternal,
+ null,
+ configPair.mExtendedConfig,
+ configPair.mCameraConfig);
+ useCase.updateSuggestedStreamSpec(
+ Preconditions.checkNotNull(primaryStreamSpecMap.get(useCase)),
+ null);
+ }
}
if (mAttached) {
mCameraInternal.attachUseCases(cameraUseCasesToAttach);
+ if (mSecondaryCameraInternal != null) {
+ requireNonNull(mSecondaryCameraInternal)
+ .attachUseCases(cameraUseCasesToAttach);
+ }
}
// Once UseCases are detached/attached, notify the camera.
@@ -538,7 +607,12 @@
return null;
}
- return new StreamSharing(mCameraInternal, newChildren, mUseCaseConfigFactory);
+ return new StreamSharing(mCameraInternal,
+ mSecondaryCameraInternal,
+ mLayoutSettings,
+ mSecondaryLayoutSettings,
+ newChildren,
+ mUseCaseConfigFactory);
}
}
@@ -616,8 +690,14 @@
// Ensure the current opening camera has the right camera config.
if (!mCameraUseCases.isEmpty()) {
mCameraInternal.setExtendedConfig(mCameraConfig);
+ if (mSecondaryCameraInternal != null) {
+ mSecondaryCameraInternal.setExtendedConfig(mCameraConfig);
+ }
}
mCameraInternal.attachUseCases(mCameraUseCases);
+ if (mSecondaryCameraInternal != null) {
+ mSecondaryCameraInternal.attachUseCases(mCameraUseCases);
+ }
restoreInteropConfig();
// Notify to update the use case's active state because it may be cleared if the
@@ -654,6 +734,9 @@
synchronized (mLock) {
if (mAttached) {
mCameraInternal.detachUseCases(new ArrayList<>(mCameraUseCases));
+ if (mSecondaryCameraInternal != null) {
+ mSecondaryCameraInternal.detachUseCases(new ArrayList<>(mCameraUseCases));
+ }
cacheInteropConfig();
mAttached = false;
}
@@ -754,7 +837,8 @@
cameraMode,
cameraId, existingSurfaces,
configToSupportedSizesMap,
- isPreviewStabilizationOn);
+ isPreviewStabilizationOn,
+ hasVideoCapture(newUseCases));
for (Map.Entry<UseCaseConfig<?>, UseCase> entry : configToUseCaseMap.entrySet()) {
suggestedStreamSpecs.put(entry.getValue(),
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
index 8e9b66e..8afe48f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
@@ -45,11 +45,13 @@
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.CameraEffect;
import androidx.camera.core.ImageCapture;
+import androidx.camera.core.LayoutSettings;
import androidx.camera.core.MirrorMode;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImageFormatConstants;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.MutableConfig;
@@ -63,6 +65,9 @@
import androidx.camera.core.processing.DefaultSurfaceProcessor;
import androidx.camera.core.processing.SurfaceEdge;
import androidx.camera.core.processing.SurfaceProcessorNode;
+import androidx.camera.core.processing.concurrent.DualOutConfig;
+import androidx.camera.core.processing.concurrent.DualSurfaceProcessor;
+import androidx.camera.core.processing.concurrent.DualSurfaceProcessorNode;
import androidx.camera.core.processing.util.OutConfig;
import com.google.common.util.concurrent.ListenableFuture;
@@ -84,21 +89,39 @@
@NonNull
private final VirtualCameraAdapter mVirtualCameraAdapter;
+ // The layout settings of primary camera in dual camera case.
+ @NonNull
+ private final LayoutSettings mLayoutSettings;
+ // The layout settings of secondary camera in dual camera case.
+ @NonNull
+ private final LayoutSettings mSecondaryLayoutSettings;
// Node that applies effect to the input.
@Nullable
private SurfaceProcessorNode mEffectNode;
// Node that shares a single stream to multiple UseCases.
@Nullable
private SurfaceProcessorNode mSharingNode;
+ // Node that shares dual streams to multiple UseCases.
+ @Nullable
+ private DualSurfaceProcessorNode mDualSharingNode;
// The input edge that connects to the camera.
@Nullable
private SurfaceEdge mCameraEdge;
+ // The input edge that connects to the secondary camera in dual camera case.
+ @Nullable
+ private SurfaceEdge mSecondaryCameraEdge;
// The input edge of the sharing node.
@Nullable
private SurfaceEdge mSharingInputEdge;
+ // The input edge of the secondary sharing node in dual camera case.
+ @Nullable
+ private SurfaceEdge mSecondarySharingInputEdge;
@SuppressWarnings("WeakerAccess") // Synthetic access
SessionConfig.Builder mSessionConfigBuilder;
+ @SuppressWarnings("WeakerAccess") // Synthetic access
+ SessionConfig.Builder mSecondarySessionConfigBuilder;
+
@Nullable
private SessionConfig.CloseableErrorListener mCloseableErrorListener;
@@ -124,22 +147,28 @@
* {@link UseCase}s, and a {@link UseCaseConfigFactory} for getting default {@link UseCase}
* configurations.
*/
- public StreamSharing(@NonNull CameraInternal parentCamera,
+ public StreamSharing(@NonNull CameraInternal camera,
+ @Nullable CameraInternal secondaryCamera,
+ @NonNull LayoutSettings layoutSettings,
+ @NonNull LayoutSettings secondaryLayoutSettings,
@NonNull Set<UseCase> children,
@NonNull UseCaseConfigFactory useCaseConfigFactory) {
super(getDefaultConfig(children));
mDefaultConfig = getDefaultConfig(children);
+ mLayoutSettings = layoutSettings;
+ mSecondaryLayoutSettings = secondaryLayoutSettings;
mVirtualCameraAdapter = new VirtualCameraAdapter(
- parentCamera, children, useCaseConfigFactory, (jpegQuality, rotationDegrees) -> {
- SurfaceProcessorNode sharingNode = mSharingNode;
- if (sharingNode != null) {
- return sharingNode.getSurfaceProcessor().snapshot(
- jpegQuality, rotationDegrees);
- } else {
- return Futures.immediateFailedFuture(new Exception(
- "Failed to take picture: pipeline is not ready."));
- }
- });
+ camera, secondaryCamera, children, useCaseConfigFactory,
+ (jpegQuality, rotationDegrees) -> {
+ SurfaceProcessorNode sharingNode = mSharingNode;
+ if (sharingNode != null) {
+ return sharingNode.getSurfaceProcessor().snapshot(
+ jpegQuality, rotationDegrees);
+ } else {
+ return Futures.immediateFailedFuture(new Exception(
+ "Failed to take picture: pipeline is not ready."));
+ }
+ });
}
@Nullable
@@ -174,11 +203,16 @@
@NonNull
@Override
- protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec streamSpec) {
- updateSessionConfig(createPipelineAndUpdateChildrenSpecs(
- getCameraId(), getCurrentConfig(), streamSpec));
+ protected StreamSpec onSuggestedStreamSpecUpdated(
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
+ updateSessionConfig(
+ createPipelineAndUpdateChildrenSpecs(getCameraId(),
+ getSecondaryCameraId(),
+ getCurrentConfig(),
+ primaryStreamSpec, secondaryStreamSpec));
notifyActive();
- return streamSpec;
+ return primaryStreamSpec;
}
/**
@@ -189,7 +223,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
protected StreamSpec onSuggestedStreamSpecImplementationOptionsUpdated(@NonNull Config config) {
mSessionConfigBuilder.addImplementationOptions(config);
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
return getAttachedStreamSpec().toBuilder().setImplementationOptions(config).build();
}
@@ -236,52 +270,142 @@
@NonNull
@MainThread
- private SessionConfig createPipelineAndUpdateChildrenSpecs(
+ private List<SessionConfig> createPipelineAndUpdateChildrenSpecs(
@NonNull String cameraId,
+ @Nullable String secondaryCameraId,
@NonNull UseCaseConfig<?> config,
- @NonNull StreamSpec streamSpec) {
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
checkMainThread();
- CameraInternal camera = checkNotNull(getCamera());
- // Create input edge and the node.
+
+ if (secondaryStreamSpec == null) {
+ // primary
+ createPrimaryCamera(cameraId, secondaryCameraId,
+ config, primaryStreamSpec, null);
+
+ // sharing node
+ mSharingNode = getSharingNode(requireNonNull(getCamera()), primaryStreamSpec);
+
+ boolean isViewportSet = getViewPortCropRect() != null;
+ Map<UseCase, OutConfig> outConfigMap =
+ mVirtualCameraAdapter.getChildrenOutConfigs(mSharingInputEdge,
+ getTargetRotationInternal(), isViewportSet);
+ SurfaceProcessorNode.Out out = mSharingNode.transform(
+ SurfaceProcessorNode.In.of(mSharingInputEdge,
+ new ArrayList<>(outConfigMap.values())));
+
+ Map<UseCase, SurfaceEdge> outputEdges = new HashMap<>();
+ for (Map.Entry<UseCase, OutConfig> entry : outConfigMap.entrySet()) {
+ outputEdges.put(entry.getKey(), out.get(entry.getValue()));
+ }
+
+ mVirtualCameraAdapter.setChildrenEdges(outputEdges);
+
+ return List.of(mSessionConfigBuilder.build());
+ } else {
+ // primary
+ createPrimaryCamera(cameraId, secondaryCameraId,
+ config, primaryStreamSpec, secondaryStreamSpec);
+
+ // secondary
+ createSecondaryCamera(cameraId, secondaryCameraId,
+ config, primaryStreamSpec, secondaryStreamSpec);
+
+ // sharing node
+ mDualSharingNode = getDualSharingNode(
+ getCamera(),
+ getSecondaryCamera(),
+ primaryStreamSpec, // use primary stream spec
+ mLayoutSettings,
+ mSecondaryLayoutSettings);
+ boolean isViewportSet = getViewPortCropRect() != null;
+ Map<UseCase, DualOutConfig> outConfigMap =
+ mVirtualCameraAdapter.getChildrenOutConfigs(
+ mSharingInputEdge,
+ mSecondarySharingInputEdge,
+ getTargetRotationInternal(),
+ isViewportSet);
+ DualSurfaceProcessorNode.Out out = mDualSharingNode.transform(
+ DualSurfaceProcessorNode.In.of(
+ mSharingInputEdge,
+ mSecondarySharingInputEdge,
+ new ArrayList<>(outConfigMap.values())));
+
+ Map<UseCase, SurfaceEdge> outputEdges = new HashMap<>();
+ for (Map.Entry<UseCase, DualOutConfig> entry : outConfigMap.entrySet()) {
+ outputEdges.put(entry.getKey(), out.get(entry.getValue()));
+ }
+ mVirtualCameraAdapter.setChildrenEdges(outputEdges);
+
+ return List.of(mSessionConfigBuilder.build(),
+ mSecondarySessionConfigBuilder.build());
+ }
+ }
+
+ private void createPrimaryCamera(
+ @NonNull String cameraId,
+ @Nullable String secondaryCameraId,
+ @NonNull UseCaseConfig<?> config,
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
mCameraEdge = new SurfaceEdge(
/*targets=*/PREVIEW | VIDEO_CAPTURE,
INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
- streamSpec,
+ primaryStreamSpec,
getSensorToBufferTransformMatrix(),
- camera.getHasTransform(),
- requireNonNull(getCropRect(streamSpec.getResolution())),
- getRelativeRotation(camera), // Rotation can be overridden by children.
- // Once copied, the target rotation will no longer be useful.
+ requireNonNull(getCamera()).getHasTransform(),
+ requireNonNull(getCropRect(primaryStreamSpec.getResolution())),
+ getRelativeRotation(requireNonNull(getCamera())),
ImageOutputConfig.ROTATION_NOT_SPECIFIED,
- isMirroringRequired(camera)); // Mirroring can be overridden by children.
- mSharingInputEdge = getSharingInputEdge(mCameraEdge, camera);
+ isMirroringRequired(requireNonNull(getCamera())));
+ mSharingInputEdge = getSharingInputEdge(mCameraEdge, requireNonNull(getCamera()));
- mSharingNode = getSharingNode(camera, streamSpec);
+ mSessionConfigBuilder = createSessionConfigBuilder(
+ mCameraEdge, config, primaryStreamSpec);
+ addCameraErrorListener(mSessionConfigBuilder,
+ cameraId, secondaryCameraId, config,
+ primaryStreamSpec, secondaryStreamSpec);
+ }
- // Transform the input based on virtual camera configuration.
- boolean isViewportSet = getViewPortCropRect() != null;
- Map<UseCase, OutConfig> outConfigMap =
- mVirtualCameraAdapter.getChildrenOutConfigs(mSharingInputEdge,
- getTargetRotationInternal(), isViewportSet);
- SurfaceProcessorNode.Out out = mSharingNode.transform(
- SurfaceProcessorNode.In.of(mSharingInputEdge,
- new ArrayList<>(outConfigMap.values())));
+ private void createSecondaryCamera(
+ @NonNull String cameraId,
+ @Nullable String secondaryCameraId,
+ @NonNull UseCaseConfig<?> config,
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
+ mSecondaryCameraEdge = new SurfaceEdge(
+ /*targets=*/PREVIEW | VIDEO_CAPTURE,
+ INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+ secondaryStreamSpec,
+ getSensorToBufferTransformMatrix(),
+ requireNonNull(getSecondaryCamera()).getHasTransform(),
+ requireNonNull(getCropRect(secondaryStreamSpec.getResolution())),
+ getRelativeRotation(requireNonNull(getSecondaryCamera())),
+ ImageOutputConfig.ROTATION_NOT_SPECIFIED,
+ isMirroringRequired(requireNonNull(getSecondaryCamera())));
+ mSecondarySharingInputEdge = getSharingInputEdge(mSecondaryCameraEdge,
+ requireNonNull(getSecondaryCamera()));
- // Pass the output edges to virtual camera to connect children.
- Map<UseCase, SurfaceEdge> outputEdges = new HashMap<>();
- for (Map.Entry<UseCase, OutConfig> entry : outConfigMap.entrySet()) {
- outputEdges.put(entry.getKey(), out.get(entry.getValue()));
- }
- mVirtualCameraAdapter.setChildrenEdges(outputEdges);
+ mSecondarySessionConfigBuilder = createSessionConfigBuilder(
+ mSecondaryCameraEdge, config, secondaryStreamSpec);
+ addCameraErrorListener(mSecondarySessionConfigBuilder,
+ cameraId, secondaryCameraId, config,
+ primaryStreamSpec, secondaryStreamSpec);
+ }
+ @NonNull
+ private SessionConfig.Builder createSessionConfigBuilder(
+ @NonNull SurfaceEdge surfaceEdge,
+ @NonNull UseCaseConfig<?> config,
+ @NonNull StreamSpec streamSpec) {
// Send the camera edge Surface to the camera2.
SessionConfig.Builder builder = SessionConfig.Builder.createFrom(config,
streamSpec.getResolution());
-
propagateChildrenTemplate(builder);
propagateChildrenCamera2Interop(streamSpec.getResolution(), builder);
- builder.addSurface(mCameraEdge.getDeferrableSurface(),
+ DeferrableSurface deferrableSurface = surfaceEdge.getDeferrableSurface();
+ builder.addSurface(deferrableSurface,
streamSpec.getDynamicRange(),
null,
MirrorMode.MIRROR_MODE_UNSPECIFIED);
@@ -290,9 +414,7 @@
if (streamSpec.getImplementationOptions() != null) {
builder.addImplementationOptions(streamSpec.getImplementationOptions());
}
- addCameraErrorListener(builder, cameraId, config, streamSpec);
- mSessionConfigBuilder = builder;
- return builder.build();
+ return builder;
}
private void propagateChildrenTemplate(@NonNull SessionConfig.Builder builder) {
@@ -384,6 +506,21 @@
}
}
+ @NonNull
+ private DualSurfaceProcessorNode getDualSharingNode(
+ @NonNull CameraInternal primaryCamera,
+ @NonNull CameraInternal secondaryCamera,
+ @NonNull StreamSpec streamSpec,
+ @NonNull LayoutSettings primaryLayoutSettings,
+ @NonNull LayoutSettings secondaryLayoutSettings) {
+ // TODO: handle EffectNode for dual camera case
+ return new DualSurfaceProcessorNode(primaryCamera, secondaryCamera,
+ DualSurfaceProcessor.Factory.newInstance(
+ streamSpec.getDynamicRange(),
+ primaryLayoutSettings,
+ secondaryLayoutSettings));
+ }
+
private int getRotationAppliedByEffect() {
CameraEffect effect = checkNotNull(getEffect());
if (effect.getTransformation() == CameraEffect.TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION) {
@@ -427,8 +564,10 @@
private void addCameraErrorListener(
@NonNull SessionConfig.Builder sessionConfigBuilder,
@NonNull String cameraId,
+ @Nullable String secondaryCameraId,
@NonNull UseCaseConfig<?> config,
- @NonNull StreamSpec streamSpec) {
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
if (mCloseableErrorListener != null) {
mCloseableErrorListener.close();
}
@@ -442,7 +581,8 @@
// Clear both StreamSharing and the children.
clearPipeline();
updateSessionConfig(
- createPipelineAndUpdateChildrenSpecs(cameraId, config, streamSpec));
+ createPipelineAndUpdateChildrenSpecs(cameraId, secondaryCameraId,
+ config, primaryStreamSpec, secondaryStreamSpec));
notifyReset();
// Connect the latest {@link Surface} to newly created children edges.
// Currently children UseCase does not have additional logic in SessionConfig
@@ -464,14 +604,26 @@
mCameraEdge.close();
mCameraEdge = null;
}
+ if (mSecondaryCameraEdge != null) {
+ mSecondaryCameraEdge.close();
+ mSecondaryCameraEdge = null;
+ }
if (mSharingInputEdge != null) {
mSharingInputEdge.close();
mSharingInputEdge = null;
}
+ if (mSecondarySharingInputEdge != null) {
+ mSecondarySharingInputEdge.close();
+ mSecondarySharingInputEdge = null;
+ }
if (mSharingNode != null) {
mSharingNode.release();
mSharingNode = null;
}
+ if (mDualSharingNode != null) {
+ mDualSharingNode.release();
+ mDualSharingNode = null;
+ }
if (mEffectNode != null) {
mEffectNode.release();
mEffectNode = null;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java
index 0009266..8f5f788 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java
@@ -61,6 +61,7 @@
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.impl.stabilization.StabilizationMode;
import androidx.camera.core.processing.SurfaceEdge;
+import androidx.camera.core.processing.concurrent.DualOutConfig;
import androidx.camera.core.processing.util.OutConfig;
import java.util.HashMap;
@@ -94,6 +95,9 @@
// The parent camera instance.
@NonNull
private final CameraInternal mParentCamera;
+ // The parent secondary camera instance in dual camera case.
+ @Nullable
+ private final CameraInternal mSecondaryParentCamera;
// The callback that receives the parent camera's metadata.
@NonNull
private final CameraCaptureCallback mParentMetadataCallback = createCameraCaptureCallback();
@@ -103,7 +107,8 @@
private final Map<UseCase, UseCaseConfig<?>> mChildrenConfigsMap;
@NonNull
private final ResolutionsMerger mResolutionsMerger;
-
+ @Nullable
+ private ResolutionsMerger mSecondaryResolutionsMerger;
/**
* @param parentCamera the parent {@link CameraInternal} instance. For example, the
@@ -112,18 +117,26 @@
* @param useCaseConfigFactory the factory for configuring children {@link UseCase}.
*/
VirtualCameraAdapter(@NonNull CameraInternal parentCamera,
+ @Nullable CameraInternal secondaryParentCamera,
@NonNull Set<UseCase> children,
@NonNull UseCaseConfigFactory useCaseConfigFactory,
@NonNull StreamSharing.Control streamSharingControl) {
mParentCamera = parentCamera;
+ mSecondaryParentCamera = secondaryParentCamera;
mUseCaseConfigFactory = useCaseConfigFactory;
mChildren = children;
+ // No need to create a new instance for secondary camera.
mChildrenConfigsMap = toChildrenConfigsMap(parentCamera, children, useCaseConfigFactory);
mChildrenConfigs = new HashSet<>(mChildrenConfigsMap.values());
mResolutionsMerger = new ResolutionsMerger(parentCamera, mChildrenConfigs);
+ if (mSecondaryParentCamera != null) {
+ mSecondaryResolutionsMerger = new ResolutionsMerger(
+ mSecondaryParentCamera, mChildrenConfigs);
+ }
// Set children state to inactive by default.
for (UseCase child : children) {
mChildrenActiveState.put(child, false);
+ // No need to create a new instance for secondary camera.
mChildrenVirtualCameras.put(child, new VirtualCamera(
parentCamera,
this,
@@ -175,6 +188,7 @@
useCase.bindToCamera(
requireNonNull(mChildrenVirtualCameras.get(useCase)),
null,
+ null,
useCase.getDefaultConfig(true, mUseCaseConfigFactory));
}
}
@@ -209,38 +223,78 @@
Map<UseCase, OutConfig> getChildrenOutConfigs(@NonNull SurfaceEdge sharingInputEdge,
@ImageOutputConfig.RotationValue int parentTargetRotation, boolean isViewportSet) {
Map<UseCase, OutConfig> outConfigs = new HashMap<>();
- // TODO: we might be able to extract parent rotation degrees from the input edge's
- // sensor-to-buffer matrix and the mirroring bit.
- int parentRotationDegrees = mParentCamera.getCameraInfo().getSensorRotationDegrees(
- parentTargetRotation);
- boolean parentIsMirrored = isMirrored(sharingInputEdge.getSensorToBufferTransform());
for (UseCase useCase : mChildren) {
- Pair<Rect, Size> preferredSizePair = mResolutionsMerger.getPreferredChildSizePair(
- requireNonNull(mChildrenConfigsMap.get(useCase)),
- sharingInputEdge.getCropRect(),
- getRotationDegrees(sharingInputEdge.getSensorToBufferTransform()),
- isViewportSet
- );
- Rect cropRectBeforeScaling = preferredSizePair.first;
- Size childSizeToScale = preferredSizePair.second;
-
- int childRotationDegrees = getChildRotationDegrees(useCase);
- requireNonNull(mChildrenVirtualCameras.get(useCase))
- .setRotationDegrees(childRotationDegrees);
- int childParentDelta = within360(sharingInputEdge.getRotationDegrees()
- + childRotationDegrees - parentRotationDegrees);
- outConfigs.put(useCase, OutConfig.of(
- getChildTargetType(useCase),
- getChildFormat(useCase),
- cropRectBeforeScaling,
- rotateSize(childSizeToScale, childParentDelta),
- childParentDelta,
- // Only mirror if the parent and the child disagrees.
- useCase.isMirroringRequired(mParentCamera) ^ parentIsMirrored));
+ OutConfig outConfig = calculateOutConfig(useCase, mResolutionsMerger,
+ mParentCamera, sharingInputEdge, parentTargetRotation, isViewportSet);
+ outConfigs.put(useCase, outConfig);
}
return outConfigs;
}
+ @NonNull
+ Map<UseCase, DualOutConfig> getChildrenOutConfigs(
+ @NonNull SurfaceEdge primaryInputEdge,
+ @NonNull SurfaceEdge secondaryInputEdge,
+ @ImageOutputConfig.RotationValue int parentTargetRotation,
+ boolean isViewportSet) {
+ Map<UseCase, DualOutConfig> outConfigs = new HashMap<>();
+ for (UseCase useCase : mChildren) {
+ // primary
+ OutConfig primaryOutConfig = calculateOutConfig(
+ useCase, mResolutionsMerger,
+ mParentCamera, primaryInputEdge,
+ parentTargetRotation, isViewportSet);
+ // secondary
+ OutConfig secondaryOutConfig = calculateOutConfig(
+ useCase, mSecondaryResolutionsMerger,
+ requireNonNull(mSecondaryParentCamera),
+ secondaryInputEdge,
+ parentTargetRotation, isViewportSet);
+ outConfigs.put(useCase, DualOutConfig.of(
+ primaryOutConfig, secondaryOutConfig));
+ }
+ return outConfigs;
+ }
+
+ @NonNull
+ private OutConfig calculateOutConfig(
+ @NonNull UseCase useCase,
+ @NonNull ResolutionsMerger resolutionsMerger,
+ @NonNull CameraInternal cameraInternal,
+ @Nullable SurfaceEdge cameraInputEdge,
+ @ImageOutputConfig.RotationValue int parentTargetRotation,
+ boolean isViewportSet) {
+ // TODO: we might be able to extract parent rotation degrees from the input edge's
+ // sensor-to-buffer matrix and the mirroring bit.
+ int parentRotationDegrees = cameraInternal.getCameraInfo()
+ .getSensorRotationDegrees(parentTargetRotation);
+ boolean parentIsMirrored = isMirrored(
+ cameraInputEdge.getSensorToBufferTransform());
+ Pair<Rect, Size> preferredSizePair = resolutionsMerger
+ .getPreferredChildSizePair(
+ requireNonNull(mChildrenConfigsMap.get(useCase)),
+ cameraInputEdge.getCropRect(),
+ getRotationDegrees(cameraInputEdge.getSensorToBufferTransform()),
+ isViewportSet);
+ Rect cropRectBeforeScaling = preferredSizePair.first;
+ Size childSizeToScale = preferredSizePair.second;
+
+ int childRotationDegrees = getChildRotationDegrees(useCase, cameraInternal);
+ requireNonNull(mChildrenVirtualCameras.get(useCase))
+ .setRotationDegrees(childRotationDegrees);
+ int childParentDelta = within360(cameraInputEdge.getRotationDegrees()
+ + childRotationDegrees - parentRotationDegrees);
+ return OutConfig.of(
+ getChildTargetType(useCase),
+ getChildFormat(useCase),
+ cropRectBeforeScaling,
+ rotateSize(childSizeToScale, childParentDelta),
+ childParentDelta,
+ // Only mirror if the parent and the child disagrees.
+ useCase.isMirroringRequired(cameraInternal)
+ ^ parentIsMirrored);
+ }
+
/**
* Update children {@link SurfaceEdge} calculated by {@link StreamSharing}.
*/
@@ -252,7 +306,7 @@
SurfaceEdge surfaceEdge = entry.getValue();
useCase.setViewPortCropRect(surfaceEdge.getCropRect());
useCase.setSensorToBufferTransformMatrix(surfaceEdge.getSensorToBufferTransform());
- useCase.updateSuggestedStreamSpec(surfaceEdge.getStreamSpec());
+ useCase.updateSuggestedStreamSpec(surfaceEdge.getStreamSpec(), null);
useCase.notifyState();
}
}
@@ -340,11 +394,11 @@
// --- private methods ---
@IntRange(from = 0, to = 359)
- private int getChildRotationDegrees(@NonNull UseCase child) {
+ private int getChildRotationDegrees(@NonNull UseCase child,
+ @NonNull CameraInternal cameraInternal) {
int childTargetRotation = ((ImageOutputConfig) child.getCurrentConfig())
.getTargetRotation(Surface.ROTATION_0);
- return mParentCamera.getCameraInfo().getSensorRotationDegrees(
- childTargetRotation);
+ return cameraInternal.getCameraInfo().getSensorRotationDegrees(childTargetRotation);
}
private static int getChildFormat(@NonNull UseCase useCase) {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index cadc462..1b1fad6 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -174,7 +174,7 @@
bufferFormat = ImageFormat.JPEG,
)
// Act: pipeline can be recreated without crashing.
- imageCapture.updateSuggestedStreamSpec(StreamSpec.builder(resolution).build())
+ imageCapture.updateSuggestedStreamSpec(StreamSpec.builder(resolution).build(), null)
}
@Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 6f9e1b3..3ec52f0 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -849,6 +849,7 @@
previewToDetach.bindToCamera(
camera,
null,
+ null,
previewToDetach.getDefaultConfig(
true,
cameraXConfig.getUseCaseConfigFactoryProvider(null)!!.newInstance(context)
@@ -863,7 +864,7 @@
.setImplementationOptions(streamSpecOptions)
.build()
previewToDetach.sensorToBufferTransformMatrix = sensorToBufferTransform
- previewToDetach.updateSuggestedStreamSpec(streamSpec)
+ previewToDetach.updateSuggestedStreamSpec(streamSpec, null)
return previewToDetach
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
index 3fb962a..2b2e6741 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
@@ -36,6 +36,7 @@
import androidx.camera.core.FocusMeteringAction.FLAG_AWB
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
+import androidx.camera.core.LayoutSettings
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
import androidx.camera.core.TorchState
@@ -282,7 +283,10 @@
val adapter =
CameraUseCaseAdapter(
fakeCamera,
+ null,
RestrictedCameraInfo(fakeCamera.cameraInfoInternal, extensionsConfig),
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
FakeCameraCoordinator(),
fakeManager,
FakeUseCaseConfigFactory(),
@@ -324,7 +328,15 @@
@Test(expected = CameraException::class)
fun addStreamSharing_throwsException() {
- val streamSharing = StreamSharing(fakeCamera, setOf(preview, video), useCaseConfigFactory)
+ val streamSharing =
+ StreamSharing(
+ fakeCamera,
+ null,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(preview, video),
+ useCaseConfigFactory
+ )
// Act: add use cases that can only be supported with StreamSharing
adapter.addUseCases(setOf(streamSharing, video, image))
}
@@ -584,7 +596,7 @@
val fakeUseCase = spy(FakeUseCase())
adapter.addUseCases(listOf(fakeUseCase))
verify(fakeUseCase)
- .bindToCamera(eq(fakeCamera), isNull(), any(FakeUseCaseConfig::class.java))
+ .bindToCamera(eq(fakeCamera), isNull(), isNull(), any(FakeUseCaseConfig::class.java))
}
@Test
@@ -1324,7 +1336,10 @@
val adapter =
CameraUseCaseAdapter(
cameraInternal,
+ null,
RestrictedCameraInfo(cameraInternal.cameraInfoInternal, cameraConfig),
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
cameraCoordinator,
fakeCameraDeviceSurfaceManager,
useCaseConfigFactory
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
index c3d4040..c5e0c42 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -43,6 +43,7 @@
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
import androidx.camera.core.ImageProxy
+import androidx.camera.core.LayoutSettings
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.impl.CameraCaptureCallback
@@ -108,6 +109,7 @@
FakeUseCase(FakeUseCaseConfig.Builder().setSurfaceOccupancyPriority(2).useCaseConfig)
private val useCaseConfigFactory = FakeUseCaseConfigFactory()
private val camera = FakeCamera()
+ private val secondaryCamera = FakeCamera()
private val frontCamera =
FakeCamera(null, FakeCameraInfoInternal(SENSOR_ROTATION, LENS_FACING_FRONT))
private lateinit var streamSharing: StreamSharing
@@ -128,7 +130,15 @@
fun setUp() {
sharingProcessor = FakeSurfaceProcessorInternal(mainThreadExecutor())
DefaultSurfaceProcessor.Factory.setSupplier { sharingProcessor }
- streamSharing = StreamSharing(camera, setOf(child1, child2), useCaseConfigFactory)
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(child1, child2),
+ useCaseConfigFactory
+ )
defaultConfig = streamSharing.getDefaultConfig(true, useCaseConfigFactory)!!
effectProcessor = FakeSurfaceProcessorInternal(mainThreadExecutor())
effect = FakeSurfaceEffect(PREVIEW or VIDEO_CAPTURE, effectProcessor)
@@ -158,13 +168,20 @@
val preview = Preview.Builder().build()
val videoCapture = VideoCapture.Builder(Recorder.Builder().build()).build()
streamSharing =
- StreamSharing(frontCamera, setOf(preview, videoCapture), useCaseConfigFactory)
+ StreamSharing(
+ frontCamera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(preview, videoCapture),
+ useCaseConfigFactory
+ )
streamSharing.setViewPortCropRect(cropRect)
streamSharing.effect = effect
// Act: Bind effect and get sharing input edge.
- streamSharing.bindToCamera(frontCamera, null, defaultConfig)
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.bindToCamera(frontCamera, null, null, defaultConfig)
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
// Assert: the sharing node is built with the effect's processor
val sharingProcessor =
@@ -186,12 +203,20 @@
CameraEffect.TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION,
effectProcessor
)
- streamSharing = StreamSharing(frontCamera, setOf(child1), useCaseConfigFactory)
+ streamSharing =
+ StreamSharing(
+ frontCamera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(child1),
+ useCaseConfigFactory
+ )
streamSharing.setViewPortCropRect(cropRect)
streamSharing.effect = effect
// Act: Bind effect and get sharing input edge.
- streamSharing.bindToCamera(frontCamera, null, defaultConfig)
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.bindToCamera(frontCamera, null, null, defaultConfig)
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
// Assert: no remaining rotation because it's handled by the effect.
assertThat(streamSharing.sharingInputEdge!!.rotationDegrees).isEqualTo(0)
assertThat(streamSharing.sharingInputEdge!!.cropRect).isEqualTo(Rect(100, 50, 500, 650))
@@ -201,12 +226,20 @@
@Test
fun effectDoNotHandleRotationAndMirroring_remainingTransformationIsNotEmpty() {
// Arrange: create an effect that does not handle rotation.
- streamSharing = StreamSharing(camera, setOf(child1), useCaseConfigFactory)
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(child1),
+ useCaseConfigFactory
+ )
streamSharing.setViewPortCropRect(cropRect)
streamSharing.effect = effect
// Act: bind effect.
- streamSharing.bindToCamera(frontCamera, null, defaultConfig)
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.bindToCamera(frontCamera, null, null, defaultConfig)
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
// Assert: the remaining rotation still exists because the effect doesn't handle it. It will
// be handled by downstream pipeline.
assertThat(streamSharing.sharingInputEdge!!.rotationDegrees).isEqualTo(SENSOR_ROTATION)
@@ -223,11 +256,19 @@
CameraEffect.TRANSFORMATION_PASSTHROUGH,
effectProcessor
)
- streamSharing = StreamSharing(camera, setOf(child1), useCaseConfigFactory)
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(child1),
+ useCaseConfigFactory
+ )
streamSharing.effect = effect
// Act: bind effect.
- streamSharing.bindToCamera(frontCamera, null, defaultConfig)
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.bindToCamera(frontCamera, null, null, defaultConfig)
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
// Assert: surface processor is not applied, the sharing input edge is the camera edge.
assertThat(streamSharing.sharingInputEdge).isEqualTo(streamSharing.cameraEdge)
}
@@ -313,9 +354,17 @@
// Arrange: set up StreamSharing with min latency ImageCapture as child
val imageCapture =
ImageCapture.Builder().setCaptureMode(CAPTURE_MODE_MINIMIZE_LATENCY).build()
- streamSharing = StreamSharing(camera, setOf(child1, imageCapture), useCaseConfigFactory)
- streamSharing.bindToCamera(camera, null, defaultConfig)
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(child1, imageCapture),
+ useCaseConfigFactory
+ )
+ streamSharing.bindToCamera(camera, null, null, defaultConfig)
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
imageCapture.targetRotation = Surface.ROTATION_90
// Act: the child takes a picture.
@@ -363,7 +412,14 @@
.useCaseConfig
)
streamSharing =
- StreamSharing(camera, setOf(unspecifiedChild, hdrChild), useCaseConfigFactory)
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(unspecifiedChild, hdrChild),
+ useCaseConfigFactory
+ )
assertThat(
streamSharing
.mergeConfigs(
@@ -392,7 +448,15 @@
.setDynamicRange(HLG_10_BIT)
.useCaseConfig
)
- streamSharing = StreamSharing(camera, setOf(sdrChild, hdrChild), useCaseConfigFactory)
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(sdrChild, hdrChild),
+ useCaseConfigFactory
+ )
streamSharing.mergeConfigs(
camera.cameraInfoInternal, /*extendedConfig*/
null, /*cameraDefaultConfig*/
@@ -415,10 +479,10 @@
@Test
fun hasEffect_createEffectNode() {
// Arrange: set an effect on StreamSharing.
- streamSharing.bindToCamera(frontCamera, null, defaultConfig)
+ streamSharing.bindToCamera(frontCamera, null, null, defaultConfig)
streamSharing.effect = effect
// Act: create pipeline
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
shadowOf(getMainLooper()).idle()
// Assert: processors received input and output Surfaces.
assertThat(effectProcessor.surfaceRequest).isNotNull()
@@ -450,8 +514,8 @@
val value = "value"
val result1 = child1.setTagBundleOnSessionConfigAsync(key, value)
val result2 = child2.setTagBundleOnSessionConfigAsync(key, value)
- streamSharing.bindToCamera(camera, null, defaultConfig)
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.bindToCamera(camera, null, null, defaultConfig)
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
// Act: feed metadata to the parent.
streamSharing.sessionConfig.repeatingCameraCaptureCallbacks
@@ -467,14 +531,23 @@
fun sessionConfigHasStreamSpecImplementationOptions_whenCreatePipeline() {
// Arrange: set up StreamSharing with ImageCapture as child
val imageCapture = ImageCapture.Builder().build()
- streamSharing = StreamSharing(camera, setOf(child1, imageCapture), useCaseConfigFactory)
- streamSharing.bindToCamera(camera, null, defaultConfig)
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(child1, imageCapture),
+ useCaseConfigFactory
+ )
+ streamSharing.bindToCamera(camera, null, null, defaultConfig)
// Act: update stream specification.
val streamSpecOptions = MutableOptionsBundle.create()
streamSpecOptions.insertOption(testImplementationOption, testImplementationOptionValue)
streamSharing.onSuggestedStreamSpecUpdated(
- StreamSpec.builder(size).setImplementationOptions(streamSpecOptions).build()
+ StreamSpec.builder(size).setImplementationOptions(streamSpecOptions).build(),
+ null
)
// Assert: the session config gets the correct implementation options from stream
@@ -492,12 +565,21 @@
// Arrange: set up StreamSharing with ImageCapture as child with initial stream
// specification implementation options.
val imageCapture = ImageCapture.Builder().build()
- streamSharing = StreamSharing(camera, setOf(child1, imageCapture), useCaseConfigFactory)
- streamSharing.bindToCamera(camera, null, defaultConfig)
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(child1, imageCapture),
+ useCaseConfigFactory
+ )
+ streamSharing.bindToCamera(camera, null, null, defaultConfig)
var streamSpecOptions = MutableOptionsBundle.create()
streamSpecOptions.insertOption(testImplementationOption, testImplementationOptionValue)
streamSharing.updateSuggestedStreamSpec(
- StreamSpec.builder(size).setImplementationOptions(streamSpecOptions).build()
+ StreamSpec.builder(size).setImplementationOptions(streamSpecOptions).build(),
+ null
)
// Act: update stream specification implementation options.
@@ -519,11 +601,19 @@
@Test
fun sessionConfigIsSdr_whenUpdateStreamSpecWithDefaultDynamicRangeSettings() {
// Arrange.
- streamSharing = StreamSharing(camera, setOf(child1), useCaseConfigFactory)
- streamSharing.bindToCamera(camera, null, defaultConfig)
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(child1),
+ useCaseConfigFactory
+ )
+ streamSharing.bindToCamera(camera, null, null, defaultConfig)
// Act: update stream specification.
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
// Assert: the session config gets the correct dynamic range.
val outputConfigs = streamSharing.sessionConfig.outputConfigs
@@ -534,12 +624,21 @@
@Test
fun sessionConfigIsHdr_whenUpdateStreamSpecWithHdr() {
// Arrange.
- streamSharing = StreamSharing(camera, setOf(child1), useCaseConfigFactory)
- streamSharing.bindToCamera(camera, null, defaultConfig)
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(child1),
+ useCaseConfigFactory
+ )
+ streamSharing.bindToCamera(camera, null, null, defaultConfig)
// Act: update stream specification.
streamSharing.onSuggestedStreamSpecUpdated(
- StreamSpec.builder(size).setDynamicRange(HLG_10_BIT).build()
+ StreamSpec.builder(size).setDynamicRange(HLG_10_BIT).build(),
+ null
)
// Assert: the session config gets the correct dynamic range.
@@ -555,11 +654,14 @@
streamSharing =
StreamSharing(
camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
setOf(previewBuilder.build()),
Camera2UseCaseConfigFactory(context)
)
- streamSharing.bindToCamera(camera, null, defaultConfig)
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.bindToCamera(camera, null, null, defaultConfig)
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
return streamSharing.sessionConfig
}
@@ -603,10 +705,10 @@
@Test
fun updateStreamSpec_propagatesToChildren() {
// Arrange: bind StreamSharing to the camera.
- streamSharing.bindToCamera(camera, null, defaultConfig)
+ streamSharing.bindToCamera(camera, null, null, defaultConfig)
// Act: update suggested specs.
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
// Assert: StreamSharing pipeline created.
val node = streamSharing.sharingNode!!
@@ -633,8 +735,8 @@
@Test
fun onError_restartsPipeline() {
// Arrange: bind stream sharing and update specs.
- streamSharing.bindToCamera(camera, null, defaultConfig)
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.bindToCamera(camera, null, null, defaultConfig)
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
val cameraEdge = streamSharing.cameraEdge
val node = streamSharing.sharingNode
// Arrange: given children new Surfaces.
@@ -685,7 +787,7 @@
@Test
fun bindChildToCamera_virtualCameraHasNoTransform() {
// Act.
- streamSharing.bindToCamera(camera, null, null)
+ streamSharing.bindToCamera(camera, null, null, null)
// Assert.
assertThat(child1.camera!!.hasTransform).isFalse()
assertThat(child2.camera!!.hasTransform).isFalse()
@@ -694,7 +796,7 @@
@Test
fun bindChildToCamera_virtualCameraHasNoRotationDegrees() {
// Act.
- streamSharing.bindToCamera(frontCamera, null, null)
+ streamSharing.bindToCamera(frontCamera, null, null, null)
// Assert.
assertThat(child1.camera!!.cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
.isEqualTo(0)
@@ -708,7 +810,7 @@
assertThat(child1.camera).isNull()
assertThat(child2.camera).isNull()
// Act: bind to camera.
- streamSharing.bindToCamera(camera, null, null)
+ streamSharing.bindToCamera(camera, null, null, null)
// Assert: children bound to the virtual camera.
assertThat(child1.camera).isInstanceOf(VirtualCamera::class.java)
assertThat(child1.mergedConfigRetrieved).isTrue()
@@ -767,7 +869,15 @@
.setVideoStabilizationEnabled(false)
.build()
- streamSharing = StreamSharing(camera, setOf(preview, videoCapture), useCaseConfigFactory)
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(preview, videoCapture),
+ useCaseConfigFactory
+ )
assertThat(
streamSharing
.mergeConfigs(
@@ -788,7 +898,15 @@
.setVideoStabilizationEnabled(true)
.build()
- streamSharing = StreamSharing(camera, setOf(preview, videoCapture), useCaseConfigFactory)
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(preview, videoCapture),
+ useCaseConfigFactory
+ )
assertThat(
streamSharing
.mergeConfigs(
@@ -806,11 +924,19 @@
// Arrange.
val preview = Preview.Builder().build()
val imageCapture = ImageCapture.Builder().build()
- streamSharing = StreamSharing(camera, setOf(preview, imageCapture), useCaseConfigFactory)
- streamSharing.bindToCamera(camera, null, defaultConfig)
+ streamSharing =
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(preview, imageCapture),
+ useCaseConfigFactory
+ )
+ streamSharing.bindToCamera(camera, null, null, defaultConfig)
// Act: update stream specification.
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
// Assert:
assertThat(streamSharing.sessionConfig.templateType).isEqualTo(TEMPLATE_PREVIEW)
@@ -823,11 +949,18 @@
val imageCapture = ImageCapture.Builder().build()
val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
streamSharing =
- StreamSharing(camera, setOf(preview, imageCapture, videoCapture), useCaseConfigFactory)
- streamSharing.bindToCamera(camera, null, defaultConfig)
+ StreamSharing(
+ camera,
+ secondaryCamera,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
+ setOf(preview, imageCapture, videoCapture),
+ useCaseConfigFactory
+ )
+ streamSharing.bindToCamera(camera, null, null, defaultConfig)
// Act: update stream specification.
- streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build(), null)
// Assert:
assertThat(streamSharing.sessionConfig.templateType).isEqualTo(TEMPLATE_RECORD)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt
index 4210e6c..b5fe6ac 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt
@@ -99,8 +99,9 @@
@Before
fun setUp() {
adapter =
- VirtualCameraAdapter(parentCamera, setOf(child1, child2), useCaseConfigFactory) { _, _
- ->
+ VirtualCameraAdapter(parentCamera, null, setOf(child1, child2), useCaseConfigFactory) {
+ _,
+ _ ->
snapshotTriggered = true
Futures.immediateFuture(null)
}
@@ -162,9 +163,10 @@
useCase.bindToCamera(
parentCamera,
null,
+ null,
useCase.getDefaultConfig(true, useCaseConfigFactory)
)
- useCase.updateSuggestedStreamSpec(StreamSpec.builder(INPUT_SIZE).build())
+ useCase.updateSuggestedStreamSpec(StreamSpec.builder(INPUT_SIZE).build(), null)
return VirtualCameraAdapter.getChildSurface(useCase)
}
@@ -270,6 +272,7 @@
adapter =
VirtualCameraAdapter(
parentCamera,
+ null,
setOf(preview, child2, imageCapture),
useCaseConfigFactory
) { _, _ ->
diff --git a/camera/camera-effects-still-portrait/api/1.4.0-beta03.txt b/camera/camera-effects-still-portrait/api/1.4.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-effects-still-portrait/api/1.4.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/res-1.4.0-beta03.txt b/camera/camera-effects-still-portrait/api/res-1.4.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-effects-still-portrait/api/res-1.4.0-beta03.txt
diff --git a/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta03.txt b/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-effects/api/1.4.0-beta03.txt b/camera/camera-effects/api/1.4.0-beta03.txt
new file mode 100644
index 0000000..414b558
--- /dev/null
+++ b/camera/camera-effects/api/1.4.0-beta03.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.camera.effects {
+
+ @com.google.auto.value.AutoValue public abstract class Frame {
+ ctor public Frame();
+ method public abstract android.graphics.Rect getCropRect();
+ method public android.graphics.Canvas getOverlayCanvas();
+ method @IntRange(from=0, to=359) public abstract int getRotationDegrees();
+ method public abstract android.graphics.Matrix getSensorToBufferTransform();
+ method public abstract android.util.Size getSize();
+ method public abstract long getTimestampNanos();
+ method public abstract boolean isMirroring();
+ }
+
+ public class OverlayEffect extends androidx.camera.core.CameraEffect implements java.lang.AutoCloseable {
+ ctor public OverlayEffect(int, int, android.os.Handler, androidx.core.util.Consumer<java.lang.Throwable!>);
+ method public void clearOnDrawListener();
+ method public void close();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> drawFrameAsync(long);
+ method public android.os.Handler getHandler();
+ method public int getQueueDepth();
+ method public void setOnDrawListener(androidx.arch.core.util.Function<androidx.camera.effects.Frame!,java.lang.Boolean!>);
+ field public static final int RESULT_CANCELLED_BY_CALLER = 4; // 0x4
+ field public static final int RESULT_FRAME_NOT_FOUND = 2; // 0x2
+ field public static final int RESULT_INVALID_SURFACE = 3; // 0x3
+ field public static final int RESULT_SUCCESS = 1; // 0x1
+ }
+
+}
+
diff --git a/camera/camera-effects/api/res-1.4.0-beta03.txt b/camera/camera-effects/api/res-1.4.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-effects/api/res-1.4.0-beta03.txt
diff --git a/camera/camera-effects/api/restricted_1.4.0-beta03.txt b/camera/camera-effects/api/restricted_1.4.0-beta03.txt
new file mode 100644
index 0000000..414b558
--- /dev/null
+++ b/camera/camera-effects/api/restricted_1.4.0-beta03.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.camera.effects {
+
+ @com.google.auto.value.AutoValue public abstract class Frame {
+ ctor public Frame();
+ method public abstract android.graphics.Rect getCropRect();
+ method public android.graphics.Canvas getOverlayCanvas();
+ method @IntRange(from=0, to=359) public abstract int getRotationDegrees();
+ method public abstract android.graphics.Matrix getSensorToBufferTransform();
+ method public abstract android.util.Size getSize();
+ method public abstract long getTimestampNanos();
+ method public abstract boolean isMirroring();
+ }
+
+ public class OverlayEffect extends androidx.camera.core.CameraEffect implements java.lang.AutoCloseable {
+ ctor public OverlayEffect(int, int, android.os.Handler, androidx.core.util.Consumer<java.lang.Throwable!>);
+ method public void clearOnDrawListener();
+ method public void close();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> drawFrameAsync(long);
+ method public android.os.Handler getHandler();
+ method public int getQueueDepth();
+ method public void setOnDrawListener(androidx.arch.core.util.Function<androidx.camera.effects.Frame!,java.lang.Boolean!>);
+ field public static final int RESULT_CANCELLED_BY_CALLER = 4; // 0x4
+ field public static final int RESULT_FRAME_NOT_FOUND = 2; // 0x2
+ field public static final int RESULT_INVALID_SURFACE = 3; // 0x3
+ field public static final int RESULT_SUCCESS = 1; // 0x1
+ }
+
+}
+
diff --git a/camera/camera-extensions/api/1.4.0-beta03.txt b/camera/camera-extensions/api/1.4.0-beta03.txt
new file mode 100644
index 0000000..c454cfc
--- /dev/null
+++ b/camera/camera-extensions/api/1.4.0-beta03.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.camera.extensions {
+
+ public interface CameraExtensionsControl {
+ method public default void setExtensionStrength(@IntRange(from=0, to=100) int);
+ }
+
+ public interface CameraExtensionsInfo {
+ method public default androidx.lifecycle.LiveData<java.lang.Integer!>? getCurrentExtensionMode();
+ method public default androidx.lifecycle.LiveData<java.lang.Integer!>? getExtensionStrength();
+ method public default boolean isCurrentExtensionModeAvailable();
+ method public default boolean isExtensionStrengthAvailable();
+ }
+
+ public final class ExtensionMode {
+ field public static final int AUTO = 5; // 0x5
+ field public static final int BOKEH = 1; // 0x1
+ field public static final int FACE_RETOUCH = 4; // 0x4
+ field public static final int HDR = 2; // 0x2
+ field public static final int NIGHT = 3; // 0x3
+ field public static final int NONE = 0; // 0x0
+ }
+
+ public final class ExtensionsManager {
+ method public androidx.camera.extensions.CameraExtensionsControl? getCameraExtensionsControl(androidx.camera.core.CameraControl);
+ method public androidx.camera.extensions.CameraExtensionsInfo getCameraExtensionsInfo(androidx.camera.core.CameraInfo);
+ method public android.util.Range<java.lang.Long!>? getEstimatedCaptureLatencyRange(androidx.camera.core.CameraSelector, int);
+ method public androidx.camera.core.CameraSelector getExtensionEnabledCameraSelector(androidx.camera.core.CameraSelector, int);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.camera.extensions.ExtensionsManager!> getInstanceAsync(android.content.Context, androidx.camera.core.CameraProvider);
+ method public boolean isExtensionAvailable(androidx.camera.core.CameraSelector, int);
+ method public boolean isImageAnalysisSupported(androidx.camera.core.CameraSelector, int);
+ }
+
+}
+
diff --git a/camera/camera-extensions/api/res-1.4.0-beta03.txt b/camera/camera-extensions/api/res-1.4.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-extensions/api/res-1.4.0-beta03.txt
diff --git a/camera/camera-extensions/api/restricted_1.4.0-beta03.txt b/camera/camera-extensions/api/restricted_1.4.0-beta03.txt
new file mode 100644
index 0000000..c454cfc
--- /dev/null
+++ b/camera/camera-extensions/api/restricted_1.4.0-beta03.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.camera.extensions {
+
+ public interface CameraExtensionsControl {
+ method public default void setExtensionStrength(@IntRange(from=0, to=100) int);
+ }
+
+ public interface CameraExtensionsInfo {
+ method public default androidx.lifecycle.LiveData<java.lang.Integer!>? getCurrentExtensionMode();
+ method public default androidx.lifecycle.LiveData<java.lang.Integer!>? getExtensionStrength();
+ method public default boolean isCurrentExtensionModeAvailable();
+ method public default boolean isExtensionStrengthAvailable();
+ }
+
+ public final class ExtensionMode {
+ field public static final int AUTO = 5; // 0x5
+ field public static final int BOKEH = 1; // 0x1
+ field public static final int FACE_RETOUCH = 4; // 0x4
+ field public static final int HDR = 2; // 0x2
+ field public static final int NIGHT = 3; // 0x3
+ field public static final int NONE = 0; // 0x0
+ }
+
+ public final class ExtensionsManager {
+ method public androidx.camera.extensions.CameraExtensionsControl? getCameraExtensionsControl(androidx.camera.core.CameraControl);
+ method public androidx.camera.extensions.CameraExtensionsInfo getCameraExtensionsInfo(androidx.camera.core.CameraInfo);
+ method public android.util.Range<java.lang.Long!>? getEstimatedCaptureLatencyRange(androidx.camera.core.CameraSelector, int);
+ method public androidx.camera.core.CameraSelector getExtensionEnabledCameraSelector(androidx.camera.core.CameraSelector, int);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.camera.extensions.ExtensionsManager!> getInstanceAsync(android.content.Context, androidx.camera.core.CameraProvider);
+ method public boolean isExtensionAvailable(androidx.camera.core.CameraSelector, int);
+ method public boolean isImageAnalysisSupported(androidx.camera.core.CameraSelector, int);
+ }
+
+}
+
diff --git a/camera/camera-feature-combination-query-play-services/api/1.4.0-beta03.txt b/camera/camera-feature-combination-query-play-services/api/1.4.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/api/1.4.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-feature-combination-query-play-services/api/res-1.4.0-beta03.txt b/camera/camera-feature-combination-query-play-services/api/res-1.4.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/api/res-1.4.0-beta03.txt
diff --git a/camera/camera-feature-combination-query-play-services/api/restricted_1.4.0-beta03.txt b/camera/camera-feature-combination-query-play-services/api/restricted_1.4.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/api/restricted_1.4.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-feature-combination-query/api/1.4.0-beta03.txt b/camera/camera-feature-combination-query/api/1.4.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-feature-combination-query/api/1.4.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-feature-combination-query/api/res-1.4.0-beta03.txt b/camera/camera-feature-combination-query/api/res-1.4.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-feature-combination-query/api/res-1.4.0-beta03.txt
diff --git a/camera/camera-feature-combination-query/api/restricted_1.4.0-beta03.txt b/camera/camera-feature-combination-query/api/restricted_1.4.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-feature-combination-query/api/restricted_1.4.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-lifecycle/api/1.4.0-beta03.txt b/camera/camera-lifecycle/api/1.4.0-beta03.txt
new file mode 100644
index 0000000..a73d56a
--- /dev/null
+++ b/camera/camera-lifecycle/api/1.4.0-beta03.txt
@@ -0,0 +1,36 @@
+// Signature format: 4.0
+package androidx.camera.lifecycle {
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraProviderConfiguration {
+ }
+
+ public final class ProcessCameraProvider implements androidx.camera.core.CameraProvider {
+ method @MainThread public androidx.camera.core.Camera bindToLifecycle(androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.camera.core.CameraSelector cameraSelector, androidx.camera.core.UseCase?... useCases);
+ method @MainThread public androidx.camera.core.Camera bindToLifecycle(androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.camera.core.CameraSelector cameraSelector, androidx.camera.core.UseCaseGroup useCaseGroup);
+ method @MainThread public androidx.camera.core.ConcurrentCamera bindToLifecycle(java.util.List<androidx.camera.core.ConcurrentCamera.SingleCameraConfig?> singleCameraConfigs);
+ method @SuppressCompatibility @androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration public static void configureInstance(androidx.camera.core.CameraXConfig cameraXConfig);
+ method public java.util.List<androidx.camera.core.CameraInfo> getAvailableCameraInfos();
+ method public java.util.List<java.util.List<androidx.camera.core.CameraInfo>> getAvailableConcurrentCameraInfos();
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.camera.lifecycle.ProcessCameraProvider> getInstance(android.content.Context context);
+ method @kotlin.jvm.Throws(exceptionClasses=CameraInfoUnavailableException::class) public boolean hasCamera(androidx.camera.core.CameraSelector cameraSelector) throws androidx.camera.core.CameraInfoUnavailableException;
+ method public boolean isBound(androidx.camera.core.UseCase useCase);
+ method @MainThread public boolean isConcurrentCameraModeOn();
+ method @VisibleForTesting public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> shutdownAsync();
+ method @MainThread public void unbind(androidx.camera.core.UseCase?... useCases);
+ method @MainThread public void unbindAll();
+ property public final java.util.List<java.util.List<androidx.camera.core.CameraInfo>> availableConcurrentCameraInfos;
+ property @MainThread public final boolean isConcurrentCameraModeOn;
+ field public static final androidx.camera.lifecycle.ProcessCameraProvider.Companion Companion;
+ }
+
+ public static final class ProcessCameraProvider.Companion {
+ method @SuppressCompatibility @androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration public void configureInstance(androidx.camera.core.CameraXConfig cameraXConfig);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.lifecycle.ProcessCameraProvider> getInstance(android.content.Context context);
+ }
+
+ public final class ProcessCameraProviderExtKt {
+ method public static suspend Object? awaitInstance(androidx.camera.lifecycle.ProcessCameraProvider.Companion, android.content.Context context, kotlin.coroutines.Continuation<? super androidx.camera.lifecycle.ProcessCameraProvider>);
+ }
+
+}
+
diff --git a/camera/camera-lifecycle/api/res-1.4.0-beta03.txt b/camera/camera-lifecycle/api/res-1.4.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-lifecycle/api/res-1.4.0-beta03.txt
diff --git a/camera/camera-lifecycle/api/restricted_1.4.0-beta03.txt b/camera/camera-lifecycle/api/restricted_1.4.0-beta03.txt
new file mode 100644
index 0000000..a73d56a
--- /dev/null
+++ b/camera/camera-lifecycle/api/restricted_1.4.0-beta03.txt
@@ -0,0 +1,36 @@
+// Signature format: 4.0
+package androidx.camera.lifecycle {
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraProviderConfiguration {
+ }
+
+ public final class ProcessCameraProvider implements androidx.camera.core.CameraProvider {
+ method @MainThread public androidx.camera.core.Camera bindToLifecycle(androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.camera.core.CameraSelector cameraSelector, androidx.camera.core.UseCase?... useCases);
+ method @MainThread public androidx.camera.core.Camera bindToLifecycle(androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.camera.core.CameraSelector cameraSelector, androidx.camera.core.UseCaseGroup useCaseGroup);
+ method @MainThread public androidx.camera.core.ConcurrentCamera bindToLifecycle(java.util.List<androidx.camera.core.ConcurrentCamera.SingleCameraConfig?> singleCameraConfigs);
+ method @SuppressCompatibility @androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration public static void configureInstance(androidx.camera.core.CameraXConfig cameraXConfig);
+ method public java.util.List<androidx.camera.core.CameraInfo> getAvailableCameraInfos();
+ method public java.util.List<java.util.List<androidx.camera.core.CameraInfo>> getAvailableConcurrentCameraInfos();
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.camera.lifecycle.ProcessCameraProvider> getInstance(android.content.Context context);
+ method @kotlin.jvm.Throws(exceptionClasses=CameraInfoUnavailableException::class) public boolean hasCamera(androidx.camera.core.CameraSelector cameraSelector) throws androidx.camera.core.CameraInfoUnavailableException;
+ method public boolean isBound(androidx.camera.core.UseCase useCase);
+ method @MainThread public boolean isConcurrentCameraModeOn();
+ method @VisibleForTesting public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> shutdownAsync();
+ method @MainThread public void unbind(androidx.camera.core.UseCase?... useCases);
+ method @MainThread public void unbindAll();
+ property public final java.util.List<java.util.List<androidx.camera.core.CameraInfo>> availableConcurrentCameraInfos;
+ property @MainThread public final boolean isConcurrentCameraModeOn;
+ field public static final androidx.camera.lifecycle.ProcessCameraProvider.Companion Companion;
+ }
+
+ public static final class ProcessCameraProvider.Companion {
+ method @SuppressCompatibility @androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration public void configureInstance(androidx.camera.core.CameraXConfig cameraXConfig);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.lifecycle.ProcessCameraProvider> getInstance(android.content.Context context);
+ }
+
+ public final class ProcessCameraProviderExtKt {
+ method public static suspend Object? awaitInstance(androidx.camera.lifecycle.ProcessCameraProvider.Companion, android.content.Context context, kotlin.coroutines.Continuation<? super androidx.camera.lifecycle.ProcessCameraProvider>);
+ }
+
+}
+
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/LifecycleCameraRepositoryTest.java b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/LifecycleCameraRepositoryTest.java
index e1df029..7400653 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/LifecycleCameraRepositoryTest.java
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/LifecycleCameraRepositoryTest.java
@@ -22,6 +22,7 @@
import static java.util.Collections.emptyList;
+import androidx.camera.core.LayoutSettings;
import androidx.camera.core.concurrent.CameraCoordinator;
import androidx.camera.core.impl.CameraConfig;
import androidx.camera.core.impl.CameraConfigs;
@@ -628,8 +629,11 @@
private CameraUseCaseAdapter createCameraUseCaseAdapterWithNewCameraConfig() {
CameraConfig cameraConfig = new FakeCameraConfig();
return new CameraUseCaseAdapter(mCamera,
+ null,
new RestrictedCameraInfo((CameraInfoInternal) mCamera.getCameraInfo(),
cameraConfig),
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
mCameraCoordinator,
new FakeCameraDeviceSurfaceManager(),
new FakeUseCaseConfigFactory());
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
index bdaa0ff..e11473b 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
@@ -38,6 +38,7 @@
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.InitializationException
+import androidx.camera.core.LayoutSettings
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
import androidx.camera.core.UseCaseGroup
@@ -514,7 +515,10 @@
lifecycleOwner,
CameraUseCaseAdapter(
cameraInternal,
+ null,
restrictedCameraInfo,
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
mCameraX!!.cameraFactory.cameraCoordinator,
mCameraX!!.cameraDeviceSurfaceManager,
mCameraX!!.defaultConfigFactory
diff --git a/camera/camera-mlkit-vision/api/1.4.0-beta03.txt b/camera/camera-mlkit-vision/api/1.4.0-beta03.txt
new file mode 100644
index 0000000..0cfc1f5
--- /dev/null
+++ b/camera/camera-mlkit-vision/api/1.4.0-beta03.txt
@@ -0,0 +1,20 @@
+// Signature format: 4.0
+package androidx.camera.mlkit.vision {
+
+ public class MlKitAnalyzer implements androidx.camera.core.ImageAnalysis.Analyzer {
+ ctor public MlKitAnalyzer(java.util.List<com.google.mlkit.vision.interfaces.Detector<? extends java.lang.Object!>!>, int, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.mlkit.vision.MlKitAnalyzer.Result!>);
+ method public final void analyze(androidx.camera.core.ImageProxy);
+ method public final android.util.Size getDefaultTargetResolution();
+ method public final int getTargetCoordinateSystem();
+ method public final void updateTransform(android.graphics.Matrix?);
+ }
+
+ public static final class MlKitAnalyzer.Result {
+ ctor public MlKitAnalyzer.Result(java.util.Map<com.google.mlkit.vision.interfaces.Detector<? extends java.lang.Object!>!,java.lang.Object!>, long, java.util.Map<com.google.mlkit.vision.interfaces.Detector<? extends java.lang.Object!>!,java.lang.Throwable!>);
+ method public Throwable? getThrowable(com.google.mlkit.vision.interfaces.Detector<? extends java.lang.Object!>);
+ method public long getTimestamp();
+ method public <T> T? getValue(com.google.mlkit.vision.interfaces.Detector<T!>);
+ }
+
+}
+
diff --git a/camera/camera-mlkit-vision/api/res-1.4.0-beta03.txt b/camera/camera-mlkit-vision/api/res-1.4.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-mlkit-vision/api/res-1.4.0-beta03.txt
diff --git a/camera/camera-mlkit-vision/api/restricted_1.4.0-beta03.txt b/camera/camera-mlkit-vision/api/restricted_1.4.0-beta03.txt
new file mode 100644
index 0000000..0cfc1f5
--- /dev/null
+++ b/camera/camera-mlkit-vision/api/restricted_1.4.0-beta03.txt
@@ -0,0 +1,20 @@
+// Signature format: 4.0
+package androidx.camera.mlkit.vision {
+
+ public class MlKitAnalyzer implements androidx.camera.core.ImageAnalysis.Analyzer {
+ ctor public MlKitAnalyzer(java.util.List<com.google.mlkit.vision.interfaces.Detector<? extends java.lang.Object!>!>, int, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.mlkit.vision.MlKitAnalyzer.Result!>);
+ method public final void analyze(androidx.camera.core.ImageProxy);
+ method public final android.util.Size getDefaultTargetResolution();
+ method public final int getTargetCoordinateSystem();
+ method public final void updateTransform(android.graphics.Matrix?);
+ }
+
+ public static final class MlKitAnalyzer.Result {
+ ctor public MlKitAnalyzer.Result(java.util.Map<com.google.mlkit.vision.interfaces.Detector<? extends java.lang.Object!>!,java.lang.Object!>, long, java.util.Map<com.google.mlkit.vision.interfaces.Detector<? extends java.lang.Object!>!,java.lang.Throwable!>);
+ method public Throwable? getThrowable(com.google.mlkit.vision.interfaces.Detector<? extends java.lang.Object!>);
+ method public long getTimestamp();
+ method public <T> T? getValue(com.google.mlkit.vision.interfaces.Detector<T!>);
+ }
+
+}
+
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CameraUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CameraUtil.java
index 264f238..c640fd6 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CameraUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CameraUtil.java
@@ -55,6 +55,7 @@
import androidx.camera.core.CameraX;
import androidx.camera.core.CameraXConfig;
import androidx.camera.core.ExperimentalRetryPolicy;
+import androidx.camera.core.LayoutSettings;
import androidx.camera.core.Logger;
import androidx.camera.core.RetryPolicy;
import androidx.camera.core.UseCase;
@@ -631,7 +632,10 @@
CameraInternal camera =
cameraSelector.select(cameraX.getCameraRepository().getCameras());
return new CameraUseCaseAdapter(camera,
+ null,
new RestrictedCameraInfo(camera.getCameraInfoInternal(), cameraConfig),
+ LayoutSettings.DEFAULT,
+ LayoutSettings.DEFAULT,
cameraCoordinator,
cameraX.getCameraDeviceSurfaceManager(),
cameraX.getDefaultConfigFactory());
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraDeviceSurfaceManager.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraDeviceSurfaceManager.java
index 38a8444..56d181e 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraDeviceSurfaceManager.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraDeviceSurfaceManager.java
@@ -89,13 +89,16 @@
@NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
@NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap,
- boolean isPreviewStabilizationOn) {
+ boolean isPreviewStabilizationOn,
+ boolean hasVideoCapture) {
List<UseCaseConfig<?>> newUseCaseConfigs =
new ArrayList<>(newUseCaseConfigsSupportedSizeMap.keySet());
checkSurfaceCombo(existingSurfaces, newUseCaseConfigs);
Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecs = new HashMap<>();
for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
- StreamSpec streamSpec = StreamSpec.builder(MAX_OUTPUT_SIZE).build();
+ StreamSpec streamSpec = StreamSpec.builder(MAX_OUTPUT_SIZE)
+ .setZslDisabled(hasVideoCapture)
+ .build();
Map<Class<? extends UseCaseConfig<?>>, StreamSpec> definedStreamSpecs =
mDefinedStreamSpecs.get(cameraId);
if (definedStreamSpecs != null) {
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCase.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCase.java
index 2d1ba6a..45ccb92 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCase.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCase.java
@@ -32,6 +32,7 @@
import androidx.core.util.Supplier;
import java.util.Collections;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
@@ -132,12 +133,14 @@
@Override
@NonNull
- protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
+ protected StreamSpec onSuggestedStreamSpecUpdated(
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
SessionConfig sessionConfig = createPipeline();
if (sessionConfig != null) {
- updateSessionConfig(sessionConfig);
+ updateSessionConfig(List.of(sessionConfig));
}
- return suggestedStreamSpec;
+ return primaryStreamSpec;
}
@Nullable
@@ -206,7 +209,7 @@
* Calls the protected method {@link UseCase#updateSessionConfig}.
*/
public void updateSessionConfigForTesting(@NonNull SessionConfig sessionConfig) {
- updateSessionConfig(sessionConfig);
+ updateSessionConfig(List.of(sessionConfig));
}
/**
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
index 1ac4332..b1c0695 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
@@ -17,9 +17,12 @@
package androidx.camera.testing.fakes;
import static android.graphics.ImageFormat.YUV_420_888;
+
import static androidx.camera.core.impl.SurfaceConfig.ConfigSize.PREVIEW;
import static androidx.camera.core.impl.SurfaceConfig.ConfigType.YUV;
+
import static com.google.common.truth.Truth.assertThat;
+
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
@@ -94,6 +97,7 @@
FAKE_CAMERA_ID0,
emptyList(),
createConfigOutputSizesMap(preview, analysis),
+ false,
false);
}
@@ -113,6 +117,7 @@
CameraMode.DEFAULT,
FAKE_CAMERA_ID0,
singletonList(analysis), createConfigOutputSizesMap(preview, video),
+ false,
false);
}
@@ -125,6 +130,7 @@
CameraMode.DEFAULT,
FAKE_CAMERA_ID0,
Collections.emptyList(), createConfigOutputSizesMap(preview, video, analysis),
+ false,
false);
}
@@ -135,12 +141,14 @@
CameraMode.DEFAULT,
FAKE_CAMERA_ID0,
emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig),
+ false,
false).first;
Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecCamera1 =
mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
CameraMode.DEFAULT,
FAKE_CAMERA_ID1,
emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig),
+ false,
false).first;
assertThat(suggestedStreamSpecsCamera0.get(mFakeUseCaseConfig)).isEqualTo(
diff --git a/camera/camera-video/api/1.4.0-beta03.txt b/camera/camera-video/api/1.4.0-beta03.txt
new file mode 100644
index 0000000..beba6c4
--- /dev/null
+++ b/camera/camera-video/api/1.4.0-beta03.txt
@@ -0,0 +1,220 @@
+// Signature format: 4.0
+package androidx.camera.video {
+
+ @com.google.auto.value.AutoValue public abstract class AudioStats {
+ method public double getAudioAmplitude();
+ method public abstract int getAudioState();
+ method public abstract Throwable? getErrorCause();
+ method public boolean hasAudio();
+ method public boolean hasError();
+ field public static final double AUDIO_AMPLITUDE_NONE = 0.0;
+ field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
+ field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
+ field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
+ field public static final int AUDIO_STATE_MUTED = 5; // 0x5
+ field public static final int AUDIO_STATE_SOURCE_ERROR = 4; // 0x4
+ field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAudioApi {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPersistentRecording {
+ }
+
+ public class FallbackStrategy {
+ method public static androidx.camera.video.FallbackStrategy higherQualityOrLowerThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy higherQualityThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy lowerQualityOrHigherThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy lowerQualityThan(androidx.camera.video.Quality);
+ }
+
+ public final class FileDescriptorOutputOptions extends androidx.camera.video.OutputOptions {
+ method public android.os.ParcelFileDescriptor getParcelFileDescriptor();
+ }
+
+ public static final class FileDescriptorOutputOptions.Builder {
+ ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
+ method public androidx.camera.video.FileDescriptorOutputOptions build();
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ public final class FileOutputOptions extends androidx.camera.video.OutputOptions {
+ method public java.io.File getFile();
+ }
+
+ public static final class FileOutputOptions.Builder {
+ ctor public FileOutputOptions.Builder(java.io.File);
+ method public androidx.camera.video.FileOutputOptions build();
+ method public androidx.camera.video.FileOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ public final class MediaStoreOutputOptions extends androidx.camera.video.OutputOptions {
+ method public android.net.Uri getCollectionUri();
+ method public android.content.ContentResolver getContentResolver();
+ method public android.content.ContentValues getContentValues();
+ field public static final android.content.ContentValues EMPTY_CONTENT_VALUES;
+ }
+
+ public static final class MediaStoreOutputOptions.Builder {
+ ctor public MediaStoreOutputOptions.Builder(android.content.ContentResolver, android.net.Uri);
+ method public androidx.camera.video.MediaStoreOutputOptions build();
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ public abstract class OutputOptions {
+ method @IntRange(from=0) public long getDurationLimitMillis();
+ method @IntRange(from=0) public long getFileSizeLimit();
+ method public android.location.Location? getLocation();
+ field public static final int DURATION_UNLIMITED = 0; // 0x0
+ field public static final int FILE_SIZE_UNLIMITED = 0; // 0x0
+ }
+
+ @com.google.auto.value.AutoValue public abstract class OutputResults {
+ ctor public OutputResults();
+ method public abstract android.net.Uri getOutputUri();
+ }
+
+ public final class PendingRecording {
+ method @SuppressCompatibility @androidx.camera.video.ExperimentalPersistentRecording public androidx.camera.video.PendingRecording asPersistentRecording();
+ method @CheckResult public androidx.camera.video.Recording start(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public androidx.camera.video.PendingRecording withAudioEnabled();
+ }
+
+ public class Quality {
+ field public static final androidx.camera.video.Quality FHD;
+ field public static final androidx.camera.video.Quality HD;
+ field public static final androidx.camera.video.Quality HIGHEST;
+ field public static final androidx.camera.video.Quality LOWEST;
+ field public static final androidx.camera.video.Quality SD;
+ field public static final androidx.camera.video.Quality UHD;
+ }
+
+ public final class QualitySelector {
+ method public static androidx.camera.video.QualitySelector from(androidx.camera.video.Quality);
+ method public static androidx.camera.video.QualitySelector from(androidx.camera.video.Quality, androidx.camera.video.FallbackStrategy);
+ method public static androidx.camera.video.QualitySelector fromOrderedList(java.util.List<androidx.camera.video.Quality!>);
+ method public static androidx.camera.video.QualitySelector fromOrderedList(java.util.List<androidx.camera.video.Quality!>, androidx.camera.video.FallbackStrategy);
+ method public static android.util.Size? getResolution(androidx.camera.core.CameraInfo, androidx.camera.video.Quality);
+ method @Deprecated public static java.util.List<androidx.camera.video.Quality!> getSupportedQualities(androidx.camera.core.CameraInfo);
+ method @Deprecated public static boolean isQualitySupported(androidx.camera.core.CameraInfo, androidx.camera.video.Quality);
+ }
+
+ public final class Recorder implements androidx.camera.video.VideoOutput {
+ method public int getAspectRatio();
+ method public java.util.concurrent.Executor? getExecutor();
+ method public androidx.camera.video.QualitySelector getQualitySelector();
+ method public int getTargetVideoEncodingBitRate();
+ method public static androidx.camera.video.VideoCapabilities getVideoCapabilities(androidx.camera.core.CameraInfo);
+ method public static androidx.camera.video.VideoCapabilities getVideoCapabilities(androidx.camera.core.CameraInfo, int);
+ method public int getVideoCapabilitiesSource();
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ method @RequiresApi(26) public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.FileDescriptorOutputOptions);
+ method public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.FileOutputOptions);
+ method public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.MediaStoreOutputOptions);
+ field public static final androidx.camera.video.QualitySelector DEFAULT_QUALITY_SELECTOR;
+ field public static final int VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE = 0; // 0x0
+ field public static final int VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES = 1; // 0x1
+ }
+
+ public static final class Recorder.Builder {
+ ctor public Recorder.Builder();
+ method public androidx.camera.video.Recorder build();
+ method public androidx.camera.video.Recorder.Builder setAspectRatio(int);
+ method public androidx.camera.video.Recorder.Builder setExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.video.Recorder.Builder setQualitySelector(androidx.camera.video.QualitySelector);
+ method public androidx.camera.video.Recorder.Builder setTargetVideoEncodingBitRate(@IntRange(from=1) int);
+ method public androidx.camera.video.Recorder.Builder setVideoCapabilitiesSource(int);
+ }
+
+ public final class Recording implements java.lang.AutoCloseable {
+ method public void close();
+ method @SuppressCompatibility @androidx.camera.video.ExperimentalPersistentRecording public boolean isPersistent();
+ method public void mute(boolean);
+ method public void pause();
+ method public void resume();
+ method public void stop();
+ }
+
+ @com.google.auto.value.AutoValue public abstract class RecordingStats {
+ method public abstract androidx.camera.video.AudioStats getAudioStats();
+ method public abstract long getNumBytesRecorded();
+ method public abstract long getRecordedDurationNanos();
+ }
+
+ public interface VideoCapabilities {
+ method public java.util.Set<androidx.camera.core.DynamicRange!> getSupportedDynamicRanges();
+ method public java.util.List<androidx.camera.video.Quality!> getSupportedQualities(androidx.camera.core.DynamicRange);
+ method public boolean isQualitySupported(androidx.camera.video.Quality, androidx.camera.core.DynamicRange);
+ method public default boolean isStabilizationSupported();
+ }
+
+ public final class VideoCapture<T extends androidx.camera.video.VideoOutput> extends androidx.camera.core.UseCase {
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public int getMirrorMode();
+ method public T getOutput();
+ method public android.util.Range<java.lang.Integer!> getTargetFrameRate();
+ method public int getTargetRotation();
+ method public boolean isVideoStabilizationEnabled();
+ method public void setTargetRotation(int);
+ method public static <T extends androidx.camera.video.VideoOutput> androidx.camera.video.VideoCapture<T!> withOutput(T);
+ }
+
+ public static final class VideoCapture.Builder<T extends androidx.camera.video.VideoOutput> implements androidx.camera.core.ExtendableBuilder<androidx.camera.video.VideoCapture!> {
+ ctor public VideoCapture.Builder(T);
+ method public androidx.camera.video.VideoCapture<T!> build();
+ method public androidx.camera.video.VideoCapture.Builder<T!> setDynamicRange(androidx.camera.core.DynamicRange);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setMirrorMode(int);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setTargetRotation(int);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setVideoStabilizationEnabled(boolean);
+ }
+
+ public interface VideoOutput {
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ }
+
+ public abstract class VideoRecordEvent {
+ method public androidx.camera.video.OutputOptions getOutputOptions();
+ method public androidx.camera.video.RecordingStats getRecordingStats();
+ }
+
+ public static final class VideoRecordEvent.Finalize extends androidx.camera.video.VideoRecordEvent {
+ method public Throwable? getCause();
+ method public int getError();
+ method public androidx.camera.video.OutputResults getOutputResults();
+ method public boolean hasError();
+ field public static final int ERROR_DURATION_LIMIT_REACHED = 9; // 0x9
+ field public static final int ERROR_ENCODING_FAILED = 6; // 0x6
+ field public static final int ERROR_FILE_SIZE_LIMIT_REACHED = 2; // 0x2
+ field public static final int ERROR_INSUFFICIENT_STORAGE = 3; // 0x3
+ field public static final int ERROR_INVALID_OUTPUT_OPTIONS = 5; // 0x5
+ field public static final int ERROR_NONE = 0; // 0x0
+ field public static final int ERROR_NO_VALID_DATA = 8; // 0x8
+ field public static final int ERROR_RECORDER_ERROR = 7; // 0x7
+ field public static final int ERROR_RECORDING_GARBAGE_COLLECTED = 10; // 0xa
+ field public static final int ERROR_SOURCE_INACTIVE = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 1; // 0x1
+ }
+
+ public static final class VideoRecordEvent.Pause extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ public static final class VideoRecordEvent.Resume extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ public static final class VideoRecordEvent.Start extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ public static final class VideoRecordEvent.Status extends androidx.camera.video.VideoRecordEvent {
+ }
+
+}
+
diff --git a/camera/camera-video/api/res-1.4.0-beta03.txt b/camera/camera-video/api/res-1.4.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-video/api/res-1.4.0-beta03.txt
diff --git a/camera/camera-video/api/restricted_1.4.0-beta03.txt b/camera/camera-video/api/restricted_1.4.0-beta03.txt
new file mode 100644
index 0000000..beba6c4
--- /dev/null
+++ b/camera/camera-video/api/restricted_1.4.0-beta03.txt
@@ -0,0 +1,220 @@
+// Signature format: 4.0
+package androidx.camera.video {
+
+ @com.google.auto.value.AutoValue public abstract class AudioStats {
+ method public double getAudioAmplitude();
+ method public abstract int getAudioState();
+ method public abstract Throwable? getErrorCause();
+ method public boolean hasAudio();
+ method public boolean hasError();
+ field public static final double AUDIO_AMPLITUDE_NONE = 0.0;
+ field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
+ field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
+ field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
+ field public static final int AUDIO_STATE_MUTED = 5; // 0x5
+ field public static final int AUDIO_STATE_SOURCE_ERROR = 4; // 0x4
+ field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAudioApi {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPersistentRecording {
+ }
+
+ public class FallbackStrategy {
+ method public static androidx.camera.video.FallbackStrategy higherQualityOrLowerThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy higherQualityThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy lowerQualityOrHigherThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy lowerQualityThan(androidx.camera.video.Quality);
+ }
+
+ public final class FileDescriptorOutputOptions extends androidx.camera.video.OutputOptions {
+ method public android.os.ParcelFileDescriptor getParcelFileDescriptor();
+ }
+
+ public static final class FileDescriptorOutputOptions.Builder {
+ ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
+ method public androidx.camera.video.FileDescriptorOutputOptions build();
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ public final class FileOutputOptions extends androidx.camera.video.OutputOptions {
+ method public java.io.File getFile();
+ }
+
+ public static final class FileOutputOptions.Builder {
+ ctor public FileOutputOptions.Builder(java.io.File);
+ method public androidx.camera.video.FileOutputOptions build();
+ method public androidx.camera.video.FileOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ public final class MediaStoreOutputOptions extends androidx.camera.video.OutputOptions {
+ method public android.net.Uri getCollectionUri();
+ method public android.content.ContentResolver getContentResolver();
+ method public android.content.ContentValues getContentValues();
+ field public static final android.content.ContentValues EMPTY_CONTENT_VALUES;
+ }
+
+ public static final class MediaStoreOutputOptions.Builder {
+ ctor public MediaStoreOutputOptions.Builder(android.content.ContentResolver, android.net.Uri);
+ method public androidx.camera.video.MediaStoreOutputOptions build();
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ public abstract class OutputOptions {
+ method @IntRange(from=0) public long getDurationLimitMillis();
+ method @IntRange(from=0) public long getFileSizeLimit();
+ method public android.location.Location? getLocation();
+ field public static final int DURATION_UNLIMITED = 0; // 0x0
+ field public static final int FILE_SIZE_UNLIMITED = 0; // 0x0
+ }
+
+ @com.google.auto.value.AutoValue public abstract class OutputResults {
+ ctor public OutputResults();
+ method public abstract android.net.Uri getOutputUri();
+ }
+
+ public final class PendingRecording {
+ method @SuppressCompatibility @androidx.camera.video.ExperimentalPersistentRecording public androidx.camera.video.PendingRecording asPersistentRecording();
+ method @CheckResult public androidx.camera.video.Recording start(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public androidx.camera.video.PendingRecording withAudioEnabled();
+ }
+
+ public class Quality {
+ field public static final androidx.camera.video.Quality FHD;
+ field public static final androidx.camera.video.Quality HD;
+ field public static final androidx.camera.video.Quality HIGHEST;
+ field public static final androidx.camera.video.Quality LOWEST;
+ field public static final androidx.camera.video.Quality SD;
+ field public static final androidx.camera.video.Quality UHD;
+ }
+
+ public final class QualitySelector {
+ method public static androidx.camera.video.QualitySelector from(androidx.camera.video.Quality);
+ method public static androidx.camera.video.QualitySelector from(androidx.camera.video.Quality, androidx.camera.video.FallbackStrategy);
+ method public static androidx.camera.video.QualitySelector fromOrderedList(java.util.List<androidx.camera.video.Quality!>);
+ method public static androidx.camera.video.QualitySelector fromOrderedList(java.util.List<androidx.camera.video.Quality!>, androidx.camera.video.FallbackStrategy);
+ method public static android.util.Size? getResolution(androidx.camera.core.CameraInfo, androidx.camera.video.Quality);
+ method @Deprecated public static java.util.List<androidx.camera.video.Quality!> getSupportedQualities(androidx.camera.core.CameraInfo);
+ method @Deprecated public static boolean isQualitySupported(androidx.camera.core.CameraInfo, androidx.camera.video.Quality);
+ }
+
+ public final class Recorder implements androidx.camera.video.VideoOutput {
+ method public int getAspectRatio();
+ method public java.util.concurrent.Executor? getExecutor();
+ method public androidx.camera.video.QualitySelector getQualitySelector();
+ method public int getTargetVideoEncodingBitRate();
+ method public static androidx.camera.video.VideoCapabilities getVideoCapabilities(androidx.camera.core.CameraInfo);
+ method public static androidx.camera.video.VideoCapabilities getVideoCapabilities(androidx.camera.core.CameraInfo, int);
+ method public int getVideoCapabilitiesSource();
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ method @RequiresApi(26) public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.FileDescriptorOutputOptions);
+ method public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.FileOutputOptions);
+ method public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.MediaStoreOutputOptions);
+ field public static final androidx.camera.video.QualitySelector DEFAULT_QUALITY_SELECTOR;
+ field public static final int VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE = 0; // 0x0
+ field public static final int VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES = 1; // 0x1
+ }
+
+ public static final class Recorder.Builder {
+ ctor public Recorder.Builder();
+ method public androidx.camera.video.Recorder build();
+ method public androidx.camera.video.Recorder.Builder setAspectRatio(int);
+ method public androidx.camera.video.Recorder.Builder setExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.video.Recorder.Builder setQualitySelector(androidx.camera.video.QualitySelector);
+ method public androidx.camera.video.Recorder.Builder setTargetVideoEncodingBitRate(@IntRange(from=1) int);
+ method public androidx.camera.video.Recorder.Builder setVideoCapabilitiesSource(int);
+ }
+
+ public final class Recording implements java.lang.AutoCloseable {
+ method public void close();
+ method @SuppressCompatibility @androidx.camera.video.ExperimentalPersistentRecording public boolean isPersistent();
+ method public void mute(boolean);
+ method public void pause();
+ method public void resume();
+ method public void stop();
+ }
+
+ @com.google.auto.value.AutoValue public abstract class RecordingStats {
+ method public abstract androidx.camera.video.AudioStats getAudioStats();
+ method public abstract long getNumBytesRecorded();
+ method public abstract long getRecordedDurationNanos();
+ }
+
+ public interface VideoCapabilities {
+ method public java.util.Set<androidx.camera.core.DynamicRange!> getSupportedDynamicRanges();
+ method public java.util.List<androidx.camera.video.Quality!> getSupportedQualities(androidx.camera.core.DynamicRange);
+ method public boolean isQualitySupported(androidx.camera.video.Quality, androidx.camera.core.DynamicRange);
+ method public default boolean isStabilizationSupported();
+ }
+
+ public final class VideoCapture<T extends androidx.camera.video.VideoOutput> extends androidx.camera.core.UseCase {
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public int getMirrorMode();
+ method public T getOutput();
+ method public android.util.Range<java.lang.Integer!> getTargetFrameRate();
+ method public int getTargetRotation();
+ method public boolean isVideoStabilizationEnabled();
+ method public void setTargetRotation(int);
+ method public static <T extends androidx.camera.video.VideoOutput> androidx.camera.video.VideoCapture<T!> withOutput(T);
+ }
+
+ public static final class VideoCapture.Builder<T extends androidx.camera.video.VideoOutput> implements androidx.camera.core.ExtendableBuilder<androidx.camera.video.VideoCapture!> {
+ ctor public VideoCapture.Builder(T);
+ method public androidx.camera.video.VideoCapture<T!> build();
+ method public androidx.camera.video.VideoCapture.Builder<T!> setDynamicRange(androidx.camera.core.DynamicRange);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setMirrorMode(int);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setTargetRotation(int);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setVideoStabilizationEnabled(boolean);
+ }
+
+ public interface VideoOutput {
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ }
+
+ public abstract class VideoRecordEvent {
+ method public androidx.camera.video.OutputOptions getOutputOptions();
+ method public androidx.camera.video.RecordingStats getRecordingStats();
+ }
+
+ public static final class VideoRecordEvent.Finalize extends androidx.camera.video.VideoRecordEvent {
+ method public Throwable? getCause();
+ method public int getError();
+ method public androidx.camera.video.OutputResults getOutputResults();
+ method public boolean hasError();
+ field public static final int ERROR_DURATION_LIMIT_REACHED = 9; // 0x9
+ field public static final int ERROR_ENCODING_FAILED = 6; // 0x6
+ field public static final int ERROR_FILE_SIZE_LIMIT_REACHED = 2; // 0x2
+ field public static final int ERROR_INSUFFICIENT_STORAGE = 3; // 0x3
+ field public static final int ERROR_INVALID_OUTPUT_OPTIONS = 5; // 0x5
+ field public static final int ERROR_NONE = 0; // 0x0
+ field public static final int ERROR_NO_VALID_DATA = 8; // 0x8
+ field public static final int ERROR_RECORDER_ERROR = 7; // 0x7
+ field public static final int ERROR_RECORDING_GARBAGE_COLLECTED = 10; // 0xa
+ field public static final int ERROR_SOURCE_INACTIVE = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 1; // 0x1
+ }
+
+ public static final class VideoRecordEvent.Pause extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ public static final class VideoRecordEvent.Resume extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ public static final class VideoRecordEvent.Start extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ public static final class VideoRecordEvent.Status extends androidx.camera.video.VideoRecordEvent {
+ }
+
+}
+
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index f02cf60..a578651 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -337,16 +337,18 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@Override
- protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
- Logger.d(TAG, "onSuggestedStreamSpecUpdated: " + suggestedStreamSpec);
+ protected StreamSpec onSuggestedStreamSpecUpdated(
+ @NonNull StreamSpec primaryStreamSpec,
+ @Nullable StreamSpec secondaryStreamSpec) {
+ Logger.d(TAG, "onSuggestedStreamSpecUpdated: " + primaryStreamSpec);
VideoCaptureConfig<T> config = (VideoCaptureConfig<T>) getCurrentConfig();
List<Size> customOrderedResolutions = config.getCustomOrderedResolutions(null);
if (customOrderedResolutions != null
- && !customOrderedResolutions.contains(suggestedStreamSpec.getResolution())) {
- Logger.w(TAG, "suggested resolution " + suggestedStreamSpec.getResolution()
+ && !customOrderedResolutions.contains(primaryStreamSpec.getResolution())) {
+ Logger.w(TAG, "suggested resolution " + primaryStreamSpec.getResolution()
+ " is not in custom ordered resolutions " + customOrderedResolutions);
}
- return suggestedStreamSpec;
+ return primaryStreamSpec;
}
/**
@@ -395,7 +397,7 @@
(VideoCaptureConfig<T>) getCurrentConfig(), attachedStreamSpec);
applyStreamInfoAndStreamSpecToSessionConfigBuilder(mSessionConfigBuilder, mStreamInfo,
attachedStreamSpec);
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
// VideoCapture has to be active to apply SessionConfig's template type.
notifyActive();
getOutput().getStreamInfo().addObserver(CameraXExecutors.mainThreadExecutor(),
@@ -463,7 +465,7 @@
@RestrictTo(Scope.LIBRARY_GROUP)
protected StreamSpec onSuggestedStreamSpecImplementationOptionsUpdated(@NonNull Config config) {
mSessionConfigBuilder.addImplementationOptions(config);
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
return requireNonNull(getAttachedStreamSpec()).toBuilder()
.setImplementationOptions(config).build();
}
@@ -771,7 +773,7 @@
Preconditions.checkNotNull(getAttachedStreamSpec()));
applyStreamInfoAndStreamSpecToSessionConfigBuilder(mSessionConfigBuilder, mStreamInfo,
getAttachedStreamSpec());
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
notifyReset();
}
@@ -872,13 +874,13 @@
applyStreamInfoAndStreamSpecToSessionConfigBuilder(mSessionConfigBuilder,
streamInfo,
attachedStreamSpec);
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
notifyReset();
} else if (currentStreamInfo.getStreamState() != streamInfo.getStreamState()) {
applyStreamInfoAndStreamSpecToSessionConfigBuilder(mSessionConfigBuilder,
streamInfo,
attachedStreamSpec);
- updateSessionConfig(mSessionConfigBuilder.build());
+ updateSessionConfig(List.of(mSessionConfigBuilder.build()));
notifyUpdated();
}
}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index ab946e1..0f51b5b 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -205,9 +205,10 @@
videoCapture.bindToCamera(
camera,
null,
+ null,
videoCapture.getDefaultConfig(true, FakeUseCaseConfigFactory())
)
- videoCapture.updateSuggestedStreamSpec(StreamSpec.builder(Size(640, 480)).build())
+ videoCapture.updateSuggestedStreamSpec(StreamSpec.builder(Size(640, 480)).build(), null)
videoCapture.onStateAttached()
// Assert: camera edge does not have transform.
assertThat(videoCapture.cameraEdge!!.hasCameraTransform()).isFalse()
diff --git a/camera/camera-view/api/1.4.0-beta03.txt b/camera/camera-view/api/1.4.0-beta03.txt
new file mode 100644
index 0000000..3b66417
--- /dev/null
+++ b/camera/camera-view/api/1.4.0-beta03.txt
@@ -0,0 +1,204 @@
+// Signature format: 4.0
+package androidx.camera.view {
+
+ public abstract class CameraController {
+ method @MainThread public void clearEffects();
+ method @MainThread public void clearImageAnalysisAnalyzer();
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method @MainThread public androidx.camera.core.CameraControl? getCameraControl();
+ method @MainThread public androidx.camera.core.CameraInfo? getCameraInfo();
+ method @MainThread public androidx.camera.core.CameraSelector getCameraSelector();
+ method @MainThread public java.util.concurrent.Executor? getImageAnalysisBackgroundExecutor();
+ method @MainThread public int getImageAnalysisBackpressureStrategy();
+ method @MainThread public int getImageAnalysisImageQueueDepth();
+ method @MainThread public int getImageAnalysisOutputImageFormat();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getImageAnalysisResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getImageAnalysisTargetSize();
+ method @MainThread public int getImageCaptureFlashMode();
+ method @MainThread public java.util.concurrent.Executor? getImageCaptureIoExecutor();
+ method @MainThread public int getImageCaptureMode();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getImageCaptureResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getImageCaptureTargetSize();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> getInitializationFuture();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getPreviewResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
+ method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTapToFocusState();
+ method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+ method @MainThread public androidx.camera.core.DynamicRange getVideoCaptureDynamicRange();
+ method @MainThread public int getVideoCaptureMirrorMode();
+ method @MainThread public androidx.camera.video.QualitySelector getVideoCaptureQualitySelector();
+ method @MainThread public android.util.Range<java.lang.Integer!> getVideoCaptureTargetFrameRate();
+ method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
+ method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
+ method @MainThread public boolean isImageAnalysisEnabled();
+ method @MainThread public boolean isImageCaptureEnabled();
+ method @MainThread public boolean isPinchToZoomEnabled();
+ method @MainThread public boolean isRecording();
+ method @MainThread public boolean isTapToFocusEnabled();
+ method @MainThread public boolean isVideoCaptureEnabled();
+ method @MainThread public void setCameraSelector(androidx.camera.core.CameraSelector);
+ method @MainThread public void setEffects(java.util.Set<androidx.camera.core.CameraEffect!>);
+ method @MainThread public void setEnabledUseCases(int);
+ method @MainThread public void setImageAnalysisAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+ method @MainThread public void setImageAnalysisBackgroundExecutor(java.util.concurrent.Executor?);
+ method @MainThread public void setImageAnalysisBackpressureStrategy(int);
+ method @MainThread public void setImageAnalysisImageQueueDepth(int);
+ method @MainThread public void setImageAnalysisOutputImageFormat(int);
+ method @MainThread public void setImageAnalysisResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setImageAnalysisTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public void setImageCaptureFlashMode(int);
+ method @MainThread public void setImageCaptureIoExecutor(java.util.concurrent.Executor?);
+ method @MainThread public void setImageCaptureMode(int);
+ method @MainThread public void setImageCaptureResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setImageCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
+ method @MainThread public void setPinchToZoomEnabled(boolean);
+ method @MainThread public void setPreviewResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public void setTapToFocusEnabled(boolean);
+ method @MainThread public void setVideoCaptureDynamicRange(androidx.camera.core.DynamicRange);
+ method @MainThread public void setVideoCaptureMirrorMode(int);
+ method @MainThread public void setVideoCaptureQualitySelector(androidx.camera.video.QualitySelector);
+ method @MainThread public void setVideoCaptureTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
+ method @MainThread @RequiresApi(26) public androidx.camera.video.Recording startRecording(androidx.camera.video.FileDescriptorOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public androidx.camera.video.Recording startRecording(androidx.camera.video.FileOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public androidx.camera.video.Recording startRecording(androidx.camera.video.MediaStoreOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
+ method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
+ field @Deprecated public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
+ field public static final int IMAGE_ANALYSIS = 2; // 0x2
+ field public static final int IMAGE_CAPTURE = 1; // 0x1
+ field public static final int TAP_TO_FOCUS_FAILED = 4; // 0x4
+ field public static final int TAP_TO_FOCUS_FOCUSED = 2; // 0x2
+ field public static final int TAP_TO_FOCUS_NOT_FOCUSED = 3; // 0x3
+ field public static final int TAP_TO_FOCUS_NOT_STARTED = 0; // 0x0
+ field public static final int TAP_TO_FOCUS_STARTED = 1; // 0x1
+ field public static final int VIDEO_CAPTURE = 4; // 0x4
+ }
+
+ @Deprecated public static final class CameraController.OutputSize {
+ ctor @Deprecated public CameraController.OutputSize(android.util.Size);
+ ctor @Deprecated public CameraController.OutputSize(int);
+ method @Deprecated public int getAspectRatio();
+ method @Deprecated public android.util.Size? getResolution();
+ field @Deprecated public static final int UNASSIGNED_ASPECT_RATIO = -1; // 0xffffffff
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPreviewViewScreenFlash {
+ }
+
+ public final class LifecycleCameraController extends androidx.camera.view.CameraController {
+ ctor public LifecycleCameraController(android.content.Context);
+ method @MainThread public void bindToLifecycle(androidx.lifecycle.LifecycleOwner);
+ method @MainThread public void unbind();
+ }
+
+ public final class PreviewView extends android.widget.FrameLayout {
+ ctor @UiThread public PreviewView(android.content.Context);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?, int);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?, int, int);
+ method @UiThread public android.graphics.Bitmap? getBitmap();
+ method @UiThread public androidx.camera.view.CameraController? getController();
+ method @UiThread public androidx.camera.view.PreviewView.ImplementationMode getImplementationMode();
+ method @UiThread public androidx.camera.core.MeteringPointFactory getMeteringPointFactory();
+ method @SuppressCompatibility public androidx.camera.view.transform.OutputTransform? getOutputTransform();
+ method public androidx.lifecycle.LiveData<androidx.camera.view.PreviewView.StreamState!> getPreviewStreamState();
+ method @UiThread public androidx.camera.view.PreviewView.ScaleType getScaleType();
+ method @SuppressCompatibility @UiThread @androidx.camera.view.ExperimentalPreviewViewScreenFlash public androidx.camera.core.ImageCapture.ScreenFlash? getScreenFlash();
+ method @UiThread public android.graphics.Matrix? getSensorToViewTransform();
+ method @UiThread public androidx.camera.core.Preview.SurfaceProvider getSurfaceProvider();
+ method @UiThread public androidx.camera.core.ViewPort? getViewPort();
+ method @UiThread public androidx.camera.core.ViewPort? getViewPort(int);
+ method @UiThread public void setController(androidx.camera.view.CameraController?);
+ method @UiThread public void setImplementationMode(androidx.camera.view.PreviewView.ImplementationMode);
+ method @UiThread public void setScaleType(androidx.camera.view.PreviewView.ScaleType);
+ method @SuppressCompatibility @androidx.camera.view.ExperimentalPreviewViewScreenFlash public void setScreenFlashOverlayColor(@ColorInt int);
+ method @UiThread public void setScreenFlashWindow(android.view.Window?);
+ }
+
+ public enum PreviewView.ImplementationMode {
+ enum_constant public static final androidx.camera.view.PreviewView.ImplementationMode COMPATIBLE;
+ enum_constant public static final androidx.camera.view.PreviewView.ImplementationMode PERFORMANCE;
+ }
+
+ public enum PreviewView.ScaleType {
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_CENTER;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_END;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_START;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_CENTER;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_END;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_START;
+ }
+
+ public enum PreviewView.StreamState {
+ enum_constant public static final androidx.camera.view.PreviewView.StreamState IDLE;
+ enum_constant public static final androidx.camera.view.PreviewView.StreamState STREAMING;
+ }
+
+ public final class RotationProvider {
+ ctor public RotationProvider(android.content.Context);
+ method @CheckResult public boolean addListener(java.util.concurrent.Executor, androidx.camera.view.RotationProvider.Listener);
+ method public void removeListener(androidx.camera.view.RotationProvider.Listener);
+ }
+
+ public static interface RotationProvider.Listener {
+ method public void onRotationChanged(int);
+ }
+
+ public final class ScreenFlashView extends android.view.View {
+ ctor @UiThread public ScreenFlashView(android.content.Context);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?, int);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?, int, int);
+ method @UiThread public androidx.camera.core.ImageCapture.ScreenFlash? getScreenFlash();
+ method @UiThread public void setController(androidx.camera.view.CameraController?);
+ method @UiThread public void setScreenFlashWindow(android.view.Window?);
+ }
+
+}
+
+package androidx.camera.view.transform {
+
+ @SuppressCompatibility public final class CoordinateTransform {
+ ctor public CoordinateTransform(androidx.camera.view.transform.OutputTransform, androidx.camera.view.transform.OutputTransform);
+ method public void mapPoint(android.graphics.PointF);
+ method public void mapPoints(float[]);
+ method public void mapRect(android.graphics.RectF);
+ method public void transform(android.graphics.Matrix);
+ }
+
+ @SuppressCompatibility public final class FileTransformFactory {
+ ctor public FileTransformFactory();
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(android.content.ContentResolver, android.net.Uri) throws java.io.IOException;
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(java.io.File) throws java.io.IOException;
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(java.io.InputStream) throws java.io.IOException;
+ method public boolean isUsingExifOrientation();
+ method public void setUsingExifOrientation(boolean);
+ }
+
+ @SuppressCompatibility public final class ImageProxyTransformFactory {
+ ctor public ImageProxyTransformFactory();
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(androidx.camera.core.ImageProxy);
+ method public boolean isUsingCropRect();
+ method public boolean isUsingRotationDegrees();
+ method public void setUsingCropRect(boolean);
+ method public void setUsingRotationDegrees(boolean);
+ }
+
+ @SuppressCompatibility public final class OutputTransform {
+ }
+
+}
+
+package androidx.camera.view.video {
+
+ public class AudioConfig {
+ method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public static androidx.camera.view.video.AudioConfig create(boolean);
+ method public boolean getAudioEnabled();
+ field public static final androidx.camera.view.video.AudioConfig AUDIO_DISABLED;
+ }
+
+}
+
diff --git a/camera/camera-view/api/res-1.4.0-beta03.txt b/camera/camera-view/api/res-1.4.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-view/api/res-1.4.0-beta03.txt
diff --git a/camera/camera-view/api/restricted_1.4.0-beta03.txt b/camera/camera-view/api/restricted_1.4.0-beta03.txt
new file mode 100644
index 0000000..3b66417
--- /dev/null
+++ b/camera/camera-view/api/restricted_1.4.0-beta03.txt
@@ -0,0 +1,204 @@
+// Signature format: 4.0
+package androidx.camera.view {
+
+ public abstract class CameraController {
+ method @MainThread public void clearEffects();
+ method @MainThread public void clearImageAnalysisAnalyzer();
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method @MainThread public androidx.camera.core.CameraControl? getCameraControl();
+ method @MainThread public androidx.camera.core.CameraInfo? getCameraInfo();
+ method @MainThread public androidx.camera.core.CameraSelector getCameraSelector();
+ method @MainThread public java.util.concurrent.Executor? getImageAnalysisBackgroundExecutor();
+ method @MainThread public int getImageAnalysisBackpressureStrategy();
+ method @MainThread public int getImageAnalysisImageQueueDepth();
+ method @MainThread public int getImageAnalysisOutputImageFormat();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getImageAnalysisResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getImageAnalysisTargetSize();
+ method @MainThread public int getImageCaptureFlashMode();
+ method @MainThread public java.util.concurrent.Executor? getImageCaptureIoExecutor();
+ method @MainThread public int getImageCaptureMode();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getImageCaptureResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getImageCaptureTargetSize();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> getInitializationFuture();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getPreviewResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
+ method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTapToFocusState();
+ method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+ method @MainThread public androidx.camera.core.DynamicRange getVideoCaptureDynamicRange();
+ method @MainThread public int getVideoCaptureMirrorMode();
+ method @MainThread public androidx.camera.video.QualitySelector getVideoCaptureQualitySelector();
+ method @MainThread public android.util.Range<java.lang.Integer!> getVideoCaptureTargetFrameRate();
+ method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
+ method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
+ method @MainThread public boolean isImageAnalysisEnabled();
+ method @MainThread public boolean isImageCaptureEnabled();
+ method @MainThread public boolean isPinchToZoomEnabled();
+ method @MainThread public boolean isRecording();
+ method @MainThread public boolean isTapToFocusEnabled();
+ method @MainThread public boolean isVideoCaptureEnabled();
+ method @MainThread public void setCameraSelector(androidx.camera.core.CameraSelector);
+ method @MainThread public void setEffects(java.util.Set<androidx.camera.core.CameraEffect!>);
+ method @MainThread public void setEnabledUseCases(int);
+ method @MainThread public void setImageAnalysisAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+ method @MainThread public void setImageAnalysisBackgroundExecutor(java.util.concurrent.Executor?);
+ method @MainThread public void setImageAnalysisBackpressureStrategy(int);
+ method @MainThread public void setImageAnalysisImageQueueDepth(int);
+ method @MainThread public void setImageAnalysisOutputImageFormat(int);
+ method @MainThread public void setImageAnalysisResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setImageAnalysisTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public void setImageCaptureFlashMode(int);
+ method @MainThread public void setImageCaptureIoExecutor(java.util.concurrent.Executor?);
+ method @MainThread public void setImageCaptureMode(int);
+ method @MainThread public void setImageCaptureResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setImageCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
+ method @MainThread public void setPinchToZoomEnabled(boolean);
+ method @MainThread public void setPreviewResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public void setTapToFocusEnabled(boolean);
+ method @MainThread public void setVideoCaptureDynamicRange(androidx.camera.core.DynamicRange);
+ method @MainThread public void setVideoCaptureMirrorMode(int);
+ method @MainThread public void setVideoCaptureQualitySelector(androidx.camera.video.QualitySelector);
+ method @MainThread public void setVideoCaptureTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
+ method @MainThread @RequiresApi(26) public androidx.camera.video.Recording startRecording(androidx.camera.video.FileDescriptorOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public androidx.camera.video.Recording startRecording(androidx.camera.video.FileOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public androidx.camera.video.Recording startRecording(androidx.camera.video.MediaStoreOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
+ method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
+ field @Deprecated public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
+ field public static final int IMAGE_ANALYSIS = 2; // 0x2
+ field public static final int IMAGE_CAPTURE = 1; // 0x1
+ field public static final int TAP_TO_FOCUS_FAILED = 4; // 0x4
+ field public static final int TAP_TO_FOCUS_FOCUSED = 2; // 0x2
+ field public static final int TAP_TO_FOCUS_NOT_FOCUSED = 3; // 0x3
+ field public static final int TAP_TO_FOCUS_NOT_STARTED = 0; // 0x0
+ field public static final int TAP_TO_FOCUS_STARTED = 1; // 0x1
+ field public static final int VIDEO_CAPTURE = 4; // 0x4
+ }
+
+ @Deprecated public static final class CameraController.OutputSize {
+ ctor @Deprecated public CameraController.OutputSize(android.util.Size);
+ ctor @Deprecated public CameraController.OutputSize(int);
+ method @Deprecated public int getAspectRatio();
+ method @Deprecated public android.util.Size? getResolution();
+ field @Deprecated public static final int UNASSIGNED_ASPECT_RATIO = -1; // 0xffffffff
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPreviewViewScreenFlash {
+ }
+
+ public final class LifecycleCameraController extends androidx.camera.view.CameraController {
+ ctor public LifecycleCameraController(android.content.Context);
+ method @MainThread public void bindToLifecycle(androidx.lifecycle.LifecycleOwner);
+ method @MainThread public void unbind();
+ }
+
+ public final class PreviewView extends android.widget.FrameLayout {
+ ctor @UiThread public PreviewView(android.content.Context);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?, int);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?, int, int);
+ method @UiThread public android.graphics.Bitmap? getBitmap();
+ method @UiThread public androidx.camera.view.CameraController? getController();
+ method @UiThread public androidx.camera.view.PreviewView.ImplementationMode getImplementationMode();
+ method @UiThread public androidx.camera.core.MeteringPointFactory getMeteringPointFactory();
+ method @SuppressCompatibility public androidx.camera.view.transform.OutputTransform? getOutputTransform();
+ method public androidx.lifecycle.LiveData<androidx.camera.view.PreviewView.StreamState!> getPreviewStreamState();
+ method @UiThread public androidx.camera.view.PreviewView.ScaleType getScaleType();
+ method @SuppressCompatibility @UiThread @androidx.camera.view.ExperimentalPreviewViewScreenFlash public androidx.camera.core.ImageCapture.ScreenFlash? getScreenFlash();
+ method @UiThread public android.graphics.Matrix? getSensorToViewTransform();
+ method @UiThread public androidx.camera.core.Preview.SurfaceProvider getSurfaceProvider();
+ method @UiThread public androidx.camera.core.ViewPort? getViewPort();
+ method @UiThread public androidx.camera.core.ViewPort? getViewPort(int);
+ method @UiThread public void setController(androidx.camera.view.CameraController?);
+ method @UiThread public void setImplementationMode(androidx.camera.view.PreviewView.ImplementationMode);
+ method @UiThread public void setScaleType(androidx.camera.view.PreviewView.ScaleType);
+ method @SuppressCompatibility @androidx.camera.view.ExperimentalPreviewViewScreenFlash public void setScreenFlashOverlayColor(@ColorInt int);
+ method @UiThread public void setScreenFlashWindow(android.view.Window?);
+ }
+
+ public enum PreviewView.ImplementationMode {
+ enum_constant public static final androidx.camera.view.PreviewView.ImplementationMode COMPATIBLE;
+ enum_constant public static final androidx.camera.view.PreviewView.ImplementationMode PERFORMANCE;
+ }
+
+ public enum PreviewView.ScaleType {
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_CENTER;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_END;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_START;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_CENTER;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_END;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_START;
+ }
+
+ public enum PreviewView.StreamState {
+ enum_constant public static final androidx.camera.view.PreviewView.StreamState IDLE;
+ enum_constant public static final androidx.camera.view.PreviewView.StreamState STREAMING;
+ }
+
+ public final class RotationProvider {
+ ctor public RotationProvider(android.content.Context);
+ method @CheckResult public boolean addListener(java.util.concurrent.Executor, androidx.camera.view.RotationProvider.Listener);
+ method public void removeListener(androidx.camera.view.RotationProvider.Listener);
+ }
+
+ public static interface RotationProvider.Listener {
+ method public void onRotationChanged(int);
+ }
+
+ public final class ScreenFlashView extends android.view.View {
+ ctor @UiThread public ScreenFlashView(android.content.Context);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?, int);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?, int, int);
+ method @UiThread public androidx.camera.core.ImageCapture.ScreenFlash? getScreenFlash();
+ method @UiThread public void setController(androidx.camera.view.CameraController?);
+ method @UiThread public void setScreenFlashWindow(android.view.Window?);
+ }
+
+}
+
+package androidx.camera.view.transform {
+
+ @SuppressCompatibility public final class CoordinateTransform {
+ ctor public CoordinateTransform(androidx.camera.view.transform.OutputTransform, androidx.camera.view.transform.OutputTransform);
+ method public void mapPoint(android.graphics.PointF);
+ method public void mapPoints(float[]);
+ method public void mapRect(android.graphics.RectF);
+ method public void transform(android.graphics.Matrix);
+ }
+
+ @SuppressCompatibility public final class FileTransformFactory {
+ ctor public FileTransformFactory();
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(android.content.ContentResolver, android.net.Uri) throws java.io.IOException;
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(java.io.File) throws java.io.IOException;
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(java.io.InputStream) throws java.io.IOException;
+ method public boolean isUsingExifOrientation();
+ method public void setUsingExifOrientation(boolean);
+ }
+
+ @SuppressCompatibility public final class ImageProxyTransformFactory {
+ ctor public ImageProxyTransformFactory();
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(androidx.camera.core.ImageProxy);
+ method public boolean isUsingCropRect();
+ method public boolean isUsingRotationDegrees();
+ method public void setUsingCropRect(boolean);
+ method public void setUsingRotationDegrees(boolean);
+ }
+
+ @SuppressCompatibility public final class OutputTransform {
+ }
+
+}
+
+package androidx.camera.view.video {
+
+ public class AudioConfig {
+ method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public static androidx.camera.view.video.AudioConfig create(boolean);
+ method public boolean getAudioEnabled();
+ field public static final androidx.camera.view.video.AudioConfig AUDIO_DISABLED;
+ }
+
+}
+
diff --git a/camera/integration-tests/avsynctestapp/build.gradle b/camera/integration-tests/avsynctestapp/build.gradle
index 1b66787..c00bb2a 100644
--- a/camera/integration-tests/avsynctestapp/build.gradle
+++ b/camera/integration-tests/avsynctestapp/build.gradle
@@ -62,7 +62,7 @@
// Align dependencies in debugRuntimeClasspath and debugAndroidTestRuntimeClasspath.
androidTestImplementation("androidx.annotation:annotation-experimental:1.4.0")
androidTestImplementation("androidx.annotation:annotation:1.8.0")
- androidTestImplementation("androidx.lifecycle:lifecycle-common:2.8.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-common:2.8.3")
// Testing framework
testImplementation(libs.kotlinCoroutinesTest)
diff --git a/camera/integration-tests/extensionstestapp/build.gradle b/camera/integration-tests/extensionstestapp/build.gradle
index 5de107f7..850c2a0 100644
--- a/camera/integration-tests/extensionstestapp/build.gradle
+++ b/camera/integration-tests/extensionstestapp/build.gradle
@@ -99,6 +99,7 @@
// Copy apks to dist/apks as they are used outside of androidx test infrastructure
ApkCopyHelperKt.setupAppApkCopy(project, "debug")
+ApkCopyHelperKt.setupAppApkCopy(project, "release")
ApkCopyHelperKt.setupTestApkCopy(project)
androidx {
diff --git a/car/app/app/api/1.7.0-beta01.txt b/car/app/app/api/1.7.0-beta01.txt
index 0c87ce1..958295c 100644
--- a/car/app/app/api/1.7.0-beta01.txt
+++ b/car/app/app/api/1.7.0-beta01.txt
@@ -1061,6 +1061,7 @@
method public androidx.core.app.Person getSelf();
method public androidx.car.app.model.CarText getTitle();
method public boolean isGroupConversation();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
}
public static final class ConversationItem.Builder {
@@ -1072,6 +1073,7 @@
method public androidx.car.app.messaging.model.ConversationItem.Builder setGroupConversation(boolean);
method public androidx.car.app.messaging.model.ConversationItem.Builder setIcon(androidx.car.app.model.CarIcon);
method public androidx.car.app.messaging.model.ConversationItem.Builder setId(String);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.messaging.model.ConversationItem.Builder setIndexable(boolean);
method public androidx.car.app.messaging.model.ConversationItem.Builder setMessages(java.util.List<androidx.car.app.messaging.model.CarMessage!>);
method public androidx.car.app.messaging.model.ConversationItem.Builder setSelf(androidx.core.app.Person);
method public androidx.car.app.messaging.model.ConversationItem.Builder setTitle(androidx.car.app.model.CarText);
@@ -1314,6 +1316,7 @@
method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
method public androidx.car.app.model.CarText? getText();
method public androidx.car.app.model.CarText? getTitle();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
method public boolean isLoading();
field public static final int IMAGE_TYPE_ICON = 1; // 0x1
field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
@@ -1326,6 +1329,7 @@
method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, androidx.car.app.model.Badge);
method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int, androidx.car.app.model.Badge);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setIndexable(boolean);
method public androidx.car.app.model.GridItem.Builder setLoading(boolean);
method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
method public androidx.car.app.model.GridItem.Builder setText(androidx.car.app.model.CarText);
@@ -1654,6 +1658,7 @@
method public androidx.car.app.model.Toggle? getToggle();
method public boolean isBrowsable();
method @androidx.car.app.annotations.RequiresCarApi(5) public boolean isEnabled();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
method public androidx.car.app.model.Row row();
method public CharSequence yourBoat();
field @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public static final int IMAGE_TYPE_EXTRA_SMALL = 8; // 0x8
@@ -1673,6 +1678,7 @@
method @androidx.car.app.annotations.RequiresCarApi(5) public androidx.car.app.model.Row.Builder setEnabled(boolean);
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon);
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon, int);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.Row.Builder setIndexable(boolean);
method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
method @IntRange(from=0) @androidx.car.app.annotations.RequiresCarApi(6) public androidx.car.app.model.Row.Builder setNumericDecoration(int);
method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
@@ -1757,6 +1763,7 @@
method public java.util.List<androidx.car.app.model.Action!> getActions();
method public androidx.car.app.model.Header? getHeader();
method public java.util.List<androidx.car.app.model.Section<? extends java.lang.Object!>!> getSections();
+ method public boolean isAlphabeticalIndexingAllowed();
method public boolean isLoading();
}
@@ -1769,6 +1776,7 @@
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder clearActions();
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder clearSections();
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+ method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setAlphabeticalIndexingAllowed(boolean);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setHeader(androidx.car.app.model.Header?);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setLoading(boolean);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setSections(java.util.List<androidx.car.app.model.Section<? extends java.lang.Object!>!>);
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 0c87ce1..958295c 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -1061,6 +1061,7 @@
method public androidx.core.app.Person getSelf();
method public androidx.car.app.model.CarText getTitle();
method public boolean isGroupConversation();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
}
public static final class ConversationItem.Builder {
@@ -1072,6 +1073,7 @@
method public androidx.car.app.messaging.model.ConversationItem.Builder setGroupConversation(boolean);
method public androidx.car.app.messaging.model.ConversationItem.Builder setIcon(androidx.car.app.model.CarIcon);
method public androidx.car.app.messaging.model.ConversationItem.Builder setId(String);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.messaging.model.ConversationItem.Builder setIndexable(boolean);
method public androidx.car.app.messaging.model.ConversationItem.Builder setMessages(java.util.List<androidx.car.app.messaging.model.CarMessage!>);
method public androidx.car.app.messaging.model.ConversationItem.Builder setSelf(androidx.core.app.Person);
method public androidx.car.app.messaging.model.ConversationItem.Builder setTitle(androidx.car.app.model.CarText);
@@ -1314,6 +1316,7 @@
method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
method public androidx.car.app.model.CarText? getText();
method public androidx.car.app.model.CarText? getTitle();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
method public boolean isLoading();
field public static final int IMAGE_TYPE_ICON = 1; // 0x1
field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
@@ -1326,6 +1329,7 @@
method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, androidx.car.app.model.Badge);
method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int, androidx.car.app.model.Badge);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setIndexable(boolean);
method public androidx.car.app.model.GridItem.Builder setLoading(boolean);
method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
method public androidx.car.app.model.GridItem.Builder setText(androidx.car.app.model.CarText);
@@ -1654,6 +1658,7 @@
method public androidx.car.app.model.Toggle? getToggle();
method public boolean isBrowsable();
method @androidx.car.app.annotations.RequiresCarApi(5) public boolean isEnabled();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
method public androidx.car.app.model.Row row();
method public CharSequence yourBoat();
field @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public static final int IMAGE_TYPE_EXTRA_SMALL = 8; // 0x8
@@ -1673,6 +1678,7 @@
method @androidx.car.app.annotations.RequiresCarApi(5) public androidx.car.app.model.Row.Builder setEnabled(boolean);
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon);
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon, int);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.Row.Builder setIndexable(boolean);
method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
method @IntRange(from=0) @androidx.car.app.annotations.RequiresCarApi(6) public androidx.car.app.model.Row.Builder setNumericDecoration(int);
method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
@@ -1757,6 +1763,7 @@
method public java.util.List<androidx.car.app.model.Action!> getActions();
method public androidx.car.app.model.Header? getHeader();
method public java.util.List<androidx.car.app.model.Section<? extends java.lang.Object!>!> getSections();
+ method public boolean isAlphabeticalIndexingAllowed();
method public boolean isLoading();
}
@@ -1769,6 +1776,7 @@
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder clearActions();
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder clearSections();
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+ method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setAlphabeticalIndexingAllowed(boolean);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setHeader(androidx.car.app.model.Header?);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setLoading(boolean);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setSections(java.util.List<androidx.car.app.model.Section<? extends java.lang.Object!>!>);
diff --git a/car/app/app/api/restricted_1.7.0-beta01.txt b/car/app/app/api/restricted_1.7.0-beta01.txt
index 0c87ce1..958295c 100644
--- a/car/app/app/api/restricted_1.7.0-beta01.txt
+++ b/car/app/app/api/restricted_1.7.0-beta01.txt
@@ -1061,6 +1061,7 @@
method public androidx.core.app.Person getSelf();
method public androidx.car.app.model.CarText getTitle();
method public boolean isGroupConversation();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
}
public static final class ConversationItem.Builder {
@@ -1072,6 +1073,7 @@
method public androidx.car.app.messaging.model.ConversationItem.Builder setGroupConversation(boolean);
method public androidx.car.app.messaging.model.ConversationItem.Builder setIcon(androidx.car.app.model.CarIcon);
method public androidx.car.app.messaging.model.ConversationItem.Builder setId(String);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.messaging.model.ConversationItem.Builder setIndexable(boolean);
method public androidx.car.app.messaging.model.ConversationItem.Builder setMessages(java.util.List<androidx.car.app.messaging.model.CarMessage!>);
method public androidx.car.app.messaging.model.ConversationItem.Builder setSelf(androidx.core.app.Person);
method public androidx.car.app.messaging.model.ConversationItem.Builder setTitle(androidx.car.app.model.CarText);
@@ -1314,6 +1316,7 @@
method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
method public androidx.car.app.model.CarText? getText();
method public androidx.car.app.model.CarText? getTitle();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
method public boolean isLoading();
field public static final int IMAGE_TYPE_ICON = 1; // 0x1
field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
@@ -1326,6 +1329,7 @@
method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, androidx.car.app.model.Badge);
method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int, androidx.car.app.model.Badge);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setIndexable(boolean);
method public androidx.car.app.model.GridItem.Builder setLoading(boolean);
method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
method public androidx.car.app.model.GridItem.Builder setText(androidx.car.app.model.CarText);
@@ -1654,6 +1658,7 @@
method public androidx.car.app.model.Toggle? getToggle();
method public boolean isBrowsable();
method @androidx.car.app.annotations.RequiresCarApi(5) public boolean isEnabled();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
method public androidx.car.app.model.Row row();
method public CharSequence yourBoat();
field @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public static final int IMAGE_TYPE_EXTRA_SMALL = 8; // 0x8
@@ -1673,6 +1678,7 @@
method @androidx.car.app.annotations.RequiresCarApi(5) public androidx.car.app.model.Row.Builder setEnabled(boolean);
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon);
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon, int);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.Row.Builder setIndexable(boolean);
method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
method @IntRange(from=0) @androidx.car.app.annotations.RequiresCarApi(6) public androidx.car.app.model.Row.Builder setNumericDecoration(int);
method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
@@ -1757,6 +1763,7 @@
method public java.util.List<androidx.car.app.model.Action!> getActions();
method public androidx.car.app.model.Header? getHeader();
method public java.util.List<androidx.car.app.model.Section<? extends java.lang.Object!>!> getSections();
+ method public boolean isAlphabeticalIndexingAllowed();
method public boolean isLoading();
}
@@ -1769,6 +1776,7 @@
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder clearActions();
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder clearSections();
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+ method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setAlphabeticalIndexingAllowed(boolean);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setHeader(androidx.car.app.model.Header?);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setLoading(boolean);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setSections(java.util.List<androidx.car.app.model.Section<? extends java.lang.Object!>!>);
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 0c87ce1..958295c 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -1061,6 +1061,7 @@
method public androidx.core.app.Person getSelf();
method public androidx.car.app.model.CarText getTitle();
method public boolean isGroupConversation();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
}
public static final class ConversationItem.Builder {
@@ -1072,6 +1073,7 @@
method public androidx.car.app.messaging.model.ConversationItem.Builder setGroupConversation(boolean);
method public androidx.car.app.messaging.model.ConversationItem.Builder setIcon(androidx.car.app.model.CarIcon);
method public androidx.car.app.messaging.model.ConversationItem.Builder setId(String);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.messaging.model.ConversationItem.Builder setIndexable(boolean);
method public androidx.car.app.messaging.model.ConversationItem.Builder setMessages(java.util.List<androidx.car.app.messaging.model.CarMessage!>);
method public androidx.car.app.messaging.model.ConversationItem.Builder setSelf(androidx.core.app.Person);
method public androidx.car.app.messaging.model.ConversationItem.Builder setTitle(androidx.car.app.model.CarText);
@@ -1314,6 +1316,7 @@
method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
method public androidx.car.app.model.CarText? getText();
method public androidx.car.app.model.CarText? getTitle();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
method public boolean isLoading();
field public static final int IMAGE_TYPE_ICON = 1; // 0x1
field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
@@ -1326,6 +1329,7 @@
method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, androidx.car.app.model.Badge);
method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int, androidx.car.app.model.Badge);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.GridItem.Builder setIndexable(boolean);
method public androidx.car.app.model.GridItem.Builder setLoading(boolean);
method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
method public androidx.car.app.model.GridItem.Builder setText(androidx.car.app.model.CarText);
@@ -1654,6 +1658,7 @@
method public androidx.car.app.model.Toggle? getToggle();
method public boolean isBrowsable();
method @androidx.car.app.annotations.RequiresCarApi(5) public boolean isEnabled();
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public boolean isIndexable();
method public androidx.car.app.model.Row row();
method public CharSequence yourBoat();
field @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public static final int IMAGE_TYPE_EXTRA_SMALL = 8; // 0x8
@@ -1673,6 +1678,7 @@
method @androidx.car.app.annotations.RequiresCarApi(5) public androidx.car.app.model.Row.Builder setEnabled(boolean);
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon);
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon, int);
+ method @SuppressCompatibility @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.Row.Builder setIndexable(boolean);
method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
method @IntRange(from=0) @androidx.car.app.annotations.RequiresCarApi(6) public androidx.car.app.model.Row.Builder setNumericDecoration(int);
method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
@@ -1757,6 +1763,7 @@
method public java.util.List<androidx.car.app.model.Action!> getActions();
method public androidx.car.app.model.Header? getHeader();
method public java.util.List<androidx.car.app.model.Section<? extends java.lang.Object!>!> getSections();
+ method public boolean isAlphabeticalIndexingAllowed();
method public boolean isLoading();
}
@@ -1769,6 +1776,7 @@
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder clearActions();
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder clearSections();
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+ method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setAlphabeticalIndexingAllowed(boolean);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setHeader(androidx.car.app.model.Header?);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setLoading(boolean);
method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.car.app.model.SectionedItemTemplate.Builder setSections(java.util.List<androidx.car.app.model.Section<? extends java.lang.Object!>!>);
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
index 4458473..b5edc77 100644
--- a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
@@ -32,6 +32,8 @@
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.CarText;
import androidx.car.app.model.Item;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
import androidx.car.app.model.constraints.ActionsConstraints;
import androidx.car.app.utils.CollectionUtils;
import androidx.core.app.Person;
@@ -62,6 +64,7 @@
private final ConversationCallbackDelegate mConversationCallbackDelegate;
@NonNull
private final List<Action> mActions;
+ private final boolean mIndexable;
@Override
public int hashCode() {
@@ -72,7 +75,8 @@
mIcon,
mIsGroupConversation,
mMessages,
- mActions
+ mActions,
+ mIndexable
);
}
@@ -95,6 +99,7 @@
&& mIsGroupConversation == otherConversationItem.mIsGroupConversation
&& Objects.equals(mMessages, otherConversationItem.mMessages)
&& Objects.equals(mActions, otherConversationItem.mActions)
+ && mIndexable == otherConversationItem.mIndexable
;
}
@@ -108,6 +113,7 @@
checkState(!mMessages.isEmpty(), "Message list cannot be empty.");
this.mConversationCallbackDelegate = requireNonNull(builder.mConversationCallbackDelegate);
this.mActions = CollectionUtils.unmodifiableCopy(builder.mActions);
+ this.mIndexable = builder.mIndexable;
}
/** Default constructor for serialization. */
@@ -131,6 +137,7 @@
}
});
mActions = Collections.emptyList();
+ mIndexable = true;
}
/**
@@ -193,6 +200,24 @@
}
/**
+ * Returns whether this item should be included in an indexed list.
+ *
+ * <p>"Indexing" refers to the process of examining list contents (e.g. item titles) to sort,
+ * partition, or filter a list. Indexing is generally used for features called "Accelerators",
+ * which allow a user to quickly find a particular {@link Item} in a long list.
+ *
+ * <p>To exclude a single item from indexed lists and accelerator features, use
+ * {@link Row.Builder#setIndexable(boolean)}.
+ *
+ * <p>To enable/disable accelerators for the entire list, see the API for the particular
+ * list-like {@link Template} that you are using.
+ */
+ @ExperimentalCarApi
+ public boolean isIndexable() {
+ return mIndexable;
+ }
+
+ /**
* Verifies that a given {@link Person} has the required fields to be a message sender. Returns
* the input {@link Person} if valid, or throws an exception if invalid.
*
@@ -221,6 +246,7 @@
@Nullable
ConversationCallbackDelegate mConversationCallbackDelegate;
final List<Action> mActions;
+ boolean mIndexable = true;
/**
* Specifies a unique identifier for the conversation
@@ -318,6 +344,14 @@
return this;
}
+ /** @see #isIndexable */
+ @ExperimentalCarApi
+ @NonNull
+ public Builder setIndexable(boolean indexable) {
+ mIndexable = indexable;
+ return this;
+ }
+
/** Returns a new {@link ConversationItem} instance defined by this builder */
@NonNull
public ConversationItem build() {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
index 06304ed..0645006 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
@@ -46,7 +46,6 @@
public final class GridItem implements Item {
/**
* The type of images supported within grid items.
- *
*/
@RestrictTo(LIBRARY)
@IntDef(value = {IMAGE_TYPE_ICON, IMAGE_TYPE_LARGE})
@@ -88,6 +87,7 @@
private final OnClickDelegate mOnClickDelegate;
@Nullable
private final Badge mBadge;
+ private final boolean mIndexable;
/**
* Returns whether the grid item is in a loading state.
@@ -156,6 +156,24 @@
return mBadge;
}
+ /**
+ * Returns whether this item should be included in an indexed list.
+ *
+ * <p>"Indexing" refers to the process of examining list contents (e.g. item titles) to sort,
+ * partition, or filter a list. Indexing is generally used for features called "Accelerators",
+ * which allow a user to quickly find a particular {@link Item} in a long list.
+ *
+ * <p>To exclude a single item from indexed lists and accelerator features, use
+ * {@link Row.Builder#setIndexable(boolean)}.
+ *
+ * <p>To enable/disable accelerators for the entire list, see the API for the particular
+ * list-like {@link Template} that you are using.
+ */
+ @ExperimentalCarApi
+ public boolean isIndexable() {
+ return mIndexable;
+ }
+
@Override
@NonNull
public String toString() {
@@ -174,8 +192,15 @@
@Override
public int hashCode() {
- return Objects.hash(mIsLoading, mTitle, mImage, mImageType, mOnClickDelegate == null,
- mBadge);
+ return Objects.hash(
+ mIsLoading,
+ mTitle,
+ mImage,
+ mImageType,
+ mOnClickDelegate == null,
+ mBadge,
+ mIndexable
+ );
}
@Override
@@ -194,7 +219,8 @@
&& Objects.equals(mImage, otherGridItem.mImage)
&& Objects.equals(mOnClickDelegate == null, otherGridItem.mOnClickDelegate == null)
&& Objects.equals(mBadge, otherGridItem.mBadge)
- && mImageType == otherGridItem.mImageType;
+ && mImageType == otherGridItem.mImageType
+ && mIndexable == otherGridItem.mIndexable;
}
GridItem(Builder builder) {
@@ -205,6 +231,7 @@
mImageType = builder.mImageType;
mOnClickDelegate = builder.mOnClickDelegate;
mBadge = builder.mBadge;
+ mIndexable = builder.mIndexable;
}
/** Constructs an empty instance, used by serialization code. */
@@ -216,6 +243,7 @@
mImageType = IMAGE_TYPE_LARGE;
mOnClickDelegate = null;
mBadge = null;
+ mIndexable = true;
}
/** A builder of {@link GridItem}. */
@@ -233,6 +261,7 @@
boolean mIsLoading;
@Nullable
Badge mBadge;
+ boolean mIndexable;
/**
* Sets whether the item is in a loading state.
@@ -425,6 +454,14 @@
return this;
}
+ /** @see #isIndexable */
+ @ExperimentalCarApi
+ @NonNull
+ public Builder setIndexable(boolean indexable) {
+ mIndexable = indexable;
+ return this;
+ }
+
/**
* Constructs the {@link GridItem} defined by this builder.
*
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index d13ebfa..c1b5a4e 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -61,7 +61,6 @@
/**
* The type of images supported within rows.
- *
*/
@RestrictTo(LIBRARY)
@IntDef(value = {IMAGE_TYPE_SMALL, IMAGE_TYPE_ICON, IMAGE_TYPE_LARGE, IMAGE_TYPE_EXTRA_SMALL})
@@ -126,6 +125,7 @@
private final boolean mIsBrowsable;
@RowImageType
private final int mRowImageType;
+ private final boolean mIndexable;
/**
* Returns the title of the row or {@code null} if not set.
@@ -248,6 +248,24 @@
return YOUR_BOAT;
}
+ /**
+ * Returns whether this item should be included in an indexed list.
+ *
+ * <p>"Indexing" refers to the process of examining list contents (e.g. item titles) to sort,
+ * partition, or filter a list. Indexing is generally used for features called "Accelerators",
+ * which allow a user to quickly find a particular {@link Item} in a long list.
+ *
+ * <p>To exclude a single item from indexed lists and accelerator features, use
+ * {@link Row.Builder#setIndexable(boolean)}.
+ *
+ * <p>To enable/disable accelerators for the entire list, see the API for the particular
+ * list-like {@link Template} that you are using.
+ */
+ @ExperimentalCarApi
+ public boolean isIndexable() {
+ return mIndexable;
+ }
+
/** Returns a {@link Row} for rowing {@link #yourBoat()} */
@NonNull
public Row row() {
@@ -289,7 +307,8 @@
mMetadata,
mIsBrowsable,
mRowImageType,
- mIsEnabled);
+ mIsEnabled,
+ mIndexable);
}
@Override
@@ -311,7 +330,8 @@
&& Objects.equals(mMetadata, otherRow.mMetadata)
&& mIsBrowsable == otherRow.mIsBrowsable
&& mRowImageType == otherRow.mRowImageType
- && mIsEnabled == otherRow.isEnabled();
+ && mIsEnabled == otherRow.isEnabled()
+ && mIndexable == otherRow.mIndexable;
}
Row(Builder builder) {
@@ -326,6 +346,7 @@
mIsBrowsable = builder.mIsBrowsable;
mRowImageType = builder.mRowImageType;
mIsEnabled = builder.mIsEnabled;
+ mIndexable = builder.mIndexable;
}
/** Constructs an empty instance, used by serialization code. */
@@ -341,6 +362,7 @@
mIsBrowsable = false;
mRowImageType = IMAGE_TYPE_SMALL;
mIsEnabled = true;
+ mIndexable = true;
}
/** A builder of {@link Row}. */
@@ -361,6 +383,7 @@
boolean mIsBrowsable;
@RowImageType
int mRowImageType = IMAGE_TYPE_SMALL;
+ boolean mIndexable = true;
/**
* Sets the title of the row.
@@ -671,6 +694,14 @@
return this;
}
+ /** @see #isIndexable */
+ @ExperimentalCarApi
+ @NonNull
+ public Builder setIndexable(boolean indexable) {
+ mIndexable = indexable;
+ return this;
+ }
+
/**
* Constructs the {@link Row} defined by this builder.
*
diff --git a/car/app/app/src/main/java/androidx/car/app/model/SectionedItemTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/SectionedItemTemplate.java
index d0b6860..9f1b19c0 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/SectionedItemTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/SectionedItemTemplate.java
@@ -46,12 +46,15 @@
private final boolean mIsLoading;
+ private final boolean mIsAlphabeticalIndexingAllowed;
+
// Empty constructor for serialization
private SectionedItemTemplate() {
mSections = Collections.emptyList();
mActions = Collections.emptyList();
mHeader = null;
mIsLoading = false;
+ mIsAlphabeticalIndexingAllowed = false;
}
/** Creates a {@link SectionedItemTemplate} from the {@link Builder}. */
@@ -60,6 +63,7 @@
mActions = Collections.unmodifiableList(builder.mActions);
mHeader = builder.mHeader;
mIsLoading = builder.mIsLoading;
+ mIsAlphabeticalIndexingAllowed = builder.mIsAlphabeticalIndexingAllowed;
}
/** Returns the list of sections within this template. */
@@ -85,9 +89,30 @@
return mIsLoading;
}
+ /**
+ * Returns whether this list can be indexed alphabetically, by item title.
+ *
+ * <p>"Indexing" refers to the process of examining list contents (e.g. item titles) to sort,
+ * partition, or filter a list. Indexing is generally used for features called "Accelerators",
+ * which allow a user to quickly find a particular {@link Item} in a long list.
+ *
+ * <p>To exclude a single item from indexing, see the relevant item's API.
+ *
+ * <p>To enable/disable accelerators for the entire list, see
+ * {@link SectionedItemTemplate.Builder#setAlphabeticalIndexingAllowed(boolean)}
+ */
+ public boolean isAlphabeticalIndexingAllowed() {
+ return mIsAlphabeticalIndexingAllowed;
+ }
+
@Override
public int hashCode() {
- return Objects.hash(mSections, mActions, mHeader, mIsLoading);
+ return Objects.hash(mSections,
+ mActions,
+ mHeader,
+ mIsLoading,
+ mIsAlphabeticalIndexingAllowed
+ );
}
@Override
@@ -105,14 +130,14 @@
return Objects.equals(mSections, template.mSections)
&& Objects.equals(mActions, template.mActions)
&& Objects.equals(mHeader, template.mHeader)
- && mIsLoading == template.mIsLoading;
+ && mIsLoading == template.mIsLoading
+ && mIsAlphabeticalIndexingAllowed == template.mIsAlphabeticalIndexingAllowed;
}
@NonNull
@Override
public String toString() {
- return "SectionedItemTemplate { sections: " + mSections + ", actions: " + mActions
- + ", header: " + mHeader + ", isLoading: " + mIsLoading + " }";
+ return "SectionedItemTemplate";
}
/**
@@ -137,6 +162,7 @@
private Header mHeader = null;
private boolean mIsLoading = false;
+ private boolean mIsAlphabeticalIndexingAllowed = false;
/** Create a new {@link SectionedItemTemplate} builder. */
public Builder() {
@@ -151,6 +177,7 @@
mActions = template.mActions;
mHeader = template.mHeader;
mIsLoading = template.mIsLoading;
+ mIsAlphabeticalIndexingAllowed = template.mIsAlphabeticalIndexingAllowed;
}
/**
@@ -245,6 +272,24 @@
}
/**
+ * Sets whether this list can be indexed alphabetically, by item title.
+ *
+ * <p>"Indexing" refers to the process of examining list contents (e.g. item titles) to
+ * sort,
+ * partition, or filter a list. Indexing is generally used for features called
+ * "Accelerators",
+ * which allow a user to quickly find a particular {@link Item} in a long list.
+ *
+ * <p>To exclude a single item from indexing, see the relevant item's API.
+ */
+ @NonNull
+ @CanIgnoreReturnValue
+ public Builder setAlphabeticalIndexingAllowed(boolean alphabeticalIndexingAllowed) {
+ mIsAlphabeticalIndexingAllowed = alphabeticalIndexingAllowed;
+ return this;
+ }
+
+ /**
* Constructs a new {@link SectionedItemTemplate} from the current state of this builder,
* throwing exceptions for any invalid state.
*
diff --git a/car/app/app/src/test/java/androidx/car/app/model/SectionedItemTemplateTest.kt b/car/app/app/src/test/java/androidx/car/app/model/SectionedItemTemplateTest.kt
index 1e8e95a..57d7d4e 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/SectionedItemTemplateTest.kt
+++ b/car/app/app/src/test/java/androidx/car/app/model/SectionedItemTemplateTest.kt
@@ -80,6 +80,13 @@
}
@Test
+ fun isAlphabeticalIndexingAllowed() {
+ val template = SectionedItemTemplate.Builder().setAlphabeticalIndexingAllowed(true).build()
+
+ assertThat(template.isAlphabeticalIndexingAllowed).isTrue()
+ }
+
+ @Test
fun build_throwsException_whenLoadingAndContainsSections() {
try {
SectionedItemTemplate.Builder()
@@ -117,73 +124,75 @@
fun setActions_throwsException_whenNotFabConstrained() {
try {
// Cannot have more than 3 actions
- val actions =
- listOf(
- Action.Builder().setTitle("Action 1").setIcon(CarIcon.COMPOSE_MESSAGE).build(),
- Action.Builder().setTitle("Action 1").setIcon(CarIcon.COMPOSE_MESSAGE).build(),
- Action.Builder().setTitle("Action 1").setIcon(CarIcon.COMPOSE_MESSAGE).build()
+ SectionedItemTemplate.Builder()
+ .setActions(
+ List(3) {
+ Action.Builder()
+ .setTitle("Action $it")
+ .setIcon(CarIcon.COMPOSE_MESSAGE)
+ .build()
+ }
)
- SectionedItemTemplate.Builder().setActions(actions)
} catch (e: IllegalArgumentException) {
assertThat(e.message).contains("list exceeded max number")
}
}
@Test
- fun equals_returnsFalse_whenPassedNull() {
- val template = SectionedItemTemplate.Builder().build()
-
- assertThat(template.equals(null)).isFalse()
+ fun equals_null_returnsFalse() {
+ assertThat(buildTemplate().equals(null)).isFalse()
}
@Test
- fun equals_isReflexive() {
- val template =
- SectionedItemTemplate.Builder()
- .setSections(testSections)
- .setHeader(testHeader)
- .setActions(testActions)
- .build()
+ fun equals_sameInstance_returnsTrue() {
+ val template = createFullyPopulatedTemplate()
- @Suppress("ReplaceCallWithBinaryOperator") assertThat(template.equals(template)).isTrue()
+ assertEqual(template, template)
}
@Test
- fun equals_returnsTrue_whenSectionsHaveTheSameContent() {
- val template1 =
- SectionedItemTemplate.Builder()
- .setSections(testSections)
- .setHeader(testHeader)
- .setActions(testActions)
- .build()
- val template2 =
- SectionedItemTemplate.Builder()
- .setSections(testSections)
- .setHeader(testHeader)
- .setActions(testActions)
- .build()
-
- assertThat(template1).isEqualTo(template2)
- assertThat(template2).isEqualTo(template1)
+ fun equals_differentTemplatesSameContent_returnsTrue() {
+ assertEqual(createFullyPopulatedTemplate(), createFullyPopulatedTemplate())
}
@Test
- fun equals_returnsFalse_whenNotEqual() {
- val templates =
- listOf(
- SectionedItemTemplate.Builder().setSections(testSections).build(),
- SectionedItemTemplate.Builder().setHeader(testHeader).build(),
- SectionedItemTemplate.Builder().setActions(testActions).build()
- )
+ fun equals_oneDifferingField_returnsFalse() {
+ val minimalTemplate = buildTemplate()
- // Test all different sections against each other
- for (i in templates.indices) {
- for (j in templates.indices) {
- if (i == j) {
- continue
- }
- assertThat(templates[i]).isNotEqualTo(templates[j])
- }
- }
+ assertNotEqual(minimalTemplate, buildTemplate { setSections(testSections) })
+ assertNotEqual(minimalTemplate, buildTemplate { setHeader(testHeader) })
+ assertNotEqual(minimalTemplate, buildTemplate { setActions(testActions) })
+ assertNotEqual(minimalTemplate, buildTemplate { setLoading(true) })
+ assertNotEqual(minimalTemplate, buildTemplate { setAlphabeticalIndexingAllowed(true) })
}
+
+ @Test
+ fun toBuilder_build_returnsEquivalentObject() {
+ val template = createFullyPopulatedTemplate()
+
+ assertEqual(template, SectionedItemTemplate.Builder(template).build())
+ }
+
+ private fun assertEqual(obj1: Any, obj2: Any) {
+ assertThat(obj1).isEqualTo(obj2)
+ assertThat(obj2).isEqualTo(obj1)
+ assertThat(obj1.hashCode()).isEqualTo(obj2.hashCode())
+ }
+
+ private fun assertNotEqual(obj1: Any, obj2: Any) {
+ assertThat(obj1).isNotEqualTo(obj2)
+ assertThat(obj2).isNotEqualTo(obj1)
+ assertThat(obj1.hashCode()).isNotEqualTo(obj2.hashCode())
+ }
+
+ private fun createFullyPopulatedTemplate() =
+ SectionedItemTemplate.Builder()
+ .setSections(testSections)
+ .setHeader(testHeader)
+ .setActions(testActions)
+ .setAlphabeticalIndexingAllowed(true)
+ .build()
+
+ private fun buildTemplate(block: SectionedItemTemplate.Builder.() -> Unit = {}) =
+ SectionedItemTemplate.Builder().apply { block() }.build()
}
diff --git a/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ScatterMapBenchmarkTest.kt b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ScatterMapBenchmarkTest.kt
index 023ed69..53657a2 100644
--- a/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ScatterMapBenchmarkTest.kt
+++ b/collection/collection-benchmark/src/androidInstrumentedTest/kotlin/androidx/collection/ScatterMapBenchmarkTest.kt
@@ -65,6 +65,11 @@
benchmark.runCollectionBenchmark(ScatterMapComputeBenchmark(sourceSet))
}
+ @Test
+ fun insert_remove() {
+ benchmark.runCollectionBenchmark(ScatterMapInsertRemoveBenchmark(badHashSourceSet))
+ }
+
companion object {
@JvmStatic
@Parameters(name = "size={0}")
diff --git a/collection/collection-benchmark/src/commonMain/kotlin/androidx/collection/ScatterMapBenchmarks.kt b/collection/collection-benchmark/src/commonMain/kotlin/androidx/collection/ScatterMapBenchmarks.kt
index 20d2ba2..3471ed1b 100644
--- a/collection/collection-benchmark/src/commonMain/kotlin/androidx/collection/ScatterMapBenchmarks.kt
+++ b/collection/collection-benchmark/src/commonMain/kotlin/androidx/collection/ScatterMapBenchmarks.kt
@@ -121,6 +121,18 @@
}
}
+internal class ScatterMapInsertRemoveBenchmark(private val dataSet: Array<Int?>) :
+ CollectionBenchmark {
+ private val map = MutableScatterMap<Int?, Int?>()
+
+ override fun measuredBlock() {
+ for (testValue in dataSet) {
+ map[testValue] = testValue
+ map.remove(testValue)
+ }
+ }
+}
+
internal fun createDataSet(size: Int): Array<String> =
Array(size) { index -> (index * Random.Default.nextFloat()).toString() }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
index 621d621..6824d27 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
@@ -868,8 +868,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -899,6 +898,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
index d0e7e55..324e0d7 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
@@ -868,8 +868,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -899,6 +898,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
index 014dd71..2f2512f 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
@@ -868,8 +868,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -899,6 +898,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
index ee51aba..8582631 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
@@ -851,8 +851,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -882,6 +881,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
index 154ff82..f8562c57 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
@@ -722,8 +722,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -750,6 +749,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
index 8d5f261..71d6bb4 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
@@ -868,8 +868,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -899,6 +898,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
index 1206d9a..3974053 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
@@ -868,8 +868,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -899,6 +898,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
index c2b5895..b3877d5 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
@@ -868,8 +868,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -899,6 +898,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
index e5e408b..313d924 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
@@ -851,8 +851,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -882,6 +881,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
index 5ccadfb..8dfe391 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
@@ -720,8 +720,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -748,6 +747,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
index ba4f8aa..ba21fb5 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
@@ -868,8 +868,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -899,6 +898,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
index 19f417b..9e986ac 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
@@ -868,8 +868,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -899,6 +898,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
index 4a9d99b..52ad2ef 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
@@ -868,8 +868,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -899,6 +898,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
index a175b7a..aed8484 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
@@ -851,8 +851,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -882,6 +881,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
index 6458cac..cdf0b3e 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
@@ -721,8 +721,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -749,6 +748,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
index e167cc1..2928542 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
@@ -884,8 +884,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -915,6 +914,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
index 39e299f..4d416d88 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
@@ -884,8 +884,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -915,6 +914,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
index 48f278d..9fe0b65 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
@@ -884,8 +884,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -915,6 +914,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt
index 0665f13..023e31a 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt
@@ -1096,8 +1096,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ resizeStorage(_capacity)
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -1127,6 +1126,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt
index 0b100ad0..c393eef 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt
@@ -901,8 +901,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -929,6 +928,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified [index]. The index
* must be a valid index. This function ensures the metadata is also written in the clone area
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
index 5ada538..c6b52ef 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
@@ -750,4 +750,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableFloatFloatMap()
+
+ for (i in 0..1000000) {
+ map[i.toFloat()] = i.toFloat()
+ map.remove(i.toFloat())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
index 10bbd69..4d99c61 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
@@ -750,4 +750,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableFloatIntMap()
+
+ for (i in 0..1000000) {
+ map[i.toFloat()] = i.toInt()
+ map.remove(i.toFloat())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
index 528d5c2..6029c51 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
@@ -750,4 +750,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableFloatLongMap()
+
+ for (i in 0..1000000) {
+ map[i.toFloat()] = i.toLong()
+ map.remove(i.toFloat())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
index c496da0..572dd68 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
@@ -765,4 +765,15 @@
assertTrue(map.all { key, value -> key < 7f && value.isNotEmpty() })
assertFalse(map.all { key, _ -> key < 6f })
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableFloatObjectMap<String>()
+
+ for (i in 0..1000000) {
+ map[i.toFloat()] = i.toString()
+ map.remove(i.toFloat())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
index c4df752..e5a105e 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
@@ -550,4 +550,15 @@
assertTrue(4f in set)
assertFalse(5f in set)
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val set = mutableFloatSetOf()
+
+ for (i in 0..1000000) {
+ set.add(i.toFloat())
+ set.remove(i.toFloat())
+ assertTrue(set.capacity < 16, "Set grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
index a23a57c..cf9a934 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
@@ -750,4 +750,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableIntFloatMap()
+
+ for (i in 0..1000000) {
+ map[i.toInt()] = i.toFloat()
+ map.remove(i.toInt())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
index 693276d5..b7d318b 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
@@ -750,4 +750,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableIntIntMap()
+
+ for (i in 0..1000000) {
+ map[i.toInt()] = i.toInt()
+ map.remove(i.toInt())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
index d759538..e115dc6 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
@@ -750,4 +750,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableIntLongMap()
+
+ for (i in 0..1000000) {
+ map[i.toInt()] = i.toLong()
+ map.remove(i.toInt())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
index 3be1f8cc..9d002e6 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
@@ -765,4 +765,15 @@
assertTrue(map.all { key, value -> key < 7 && value.isNotEmpty() })
assertFalse(map.all { key, _ -> key < 6 })
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableIntObjectMap<String>()
+
+ for (i in 0..1000000) {
+ map[i.toInt()] = i.toString()
+ map.remove(i.toInt())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
index fd2de76..66c30d3 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
@@ -550,4 +550,15 @@
assertTrue(4 in set)
assertFalse(5 in set)
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val set = mutableIntSetOf()
+
+ for (i in 0..1000000) {
+ set.add(i.toInt())
+ set.remove(i.toInt())
+ assertTrue(set.capacity < 16, "Set grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
index 2312c61..507ddf0 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
@@ -750,4 +750,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableLongFloatMap()
+
+ for (i in 0..1000000) {
+ map[i.toLong()] = i.toFloat()
+ map.remove(i.toLong())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
index 1680c00..75488a2 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
@@ -750,4 +750,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableLongIntMap()
+
+ for (i in 0..1000000) {
+ map[i.toLong()] = i.toInt()
+ map.remove(i.toLong())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
index 112cc33..a22631b 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
@@ -750,4 +750,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableLongLongMap()
+
+ for (i in 0..1000000) {
+ map[i.toLong()] = i.toLong()
+ map.remove(i.toLong())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
index 6695519..f8be517 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
@@ -765,4 +765,15 @@
assertTrue(map.all { key, value -> key < 7L && value.isNotEmpty() })
assertFalse(map.all { key, _ -> key < 6L })
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableLongObjectMap<String>()
+
+ for (i in 0..1000000) {
+ map[i.toLong()] = i.toString()
+ map.remove(i.toLong())
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
index 71564a1..dc5b7e7 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
@@ -550,4 +550,15 @@
assertTrue(4L in set)
assertFalse(5L in set)
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val set = mutableLongSetOf()
+
+ for (i in 0..1000000) {
+ set.add(i.toLong())
+ set.remove(i.toLong())
+ assertTrue(set.capacity < 16, "Set grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
index 0c19aab..90244ab 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
@@ -775,4 +775,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableObjectFloatMap<Int>()
+
+ for (i in 0..1000000) {
+ map[i] = i.toFloat()
+ map.remove(i)
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
index b70d05bd..bc640eb 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
@@ -775,4 +775,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableObjectIntMap<Int>()
+
+ for (i in 0..1000000) {
+ map[i] = i.toInt()
+ map.remove(i)
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
index 594729d3..f728b95 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
@@ -775,4 +775,15 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableObjectLongMap<Int>()
+
+ for (i in 0..1000000) {
+ map[i] = i.toLong()
+ map.remove(i)
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterMapTest.kt
index 05430d4..dd370fc 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterMapTest.kt
@@ -1394,4 +1394,53 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertOneRemoveOne() {
+ val map = MutableScatterMap<Int, String>()
+
+ for (i in 0..1000000) {
+ map[i] = i.toString()
+ map.remove(i)
+ assertTrue(map.capacity < 16, "Map grew larger than 16 after step $i")
+ }
+ }
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableScatterMap<Int, String>()
+
+ for (i in 0..100) {
+ map[i] = i.toString()
+ }
+
+ for (i in 0..100) {
+ if (i % 2 == 0) {
+ map.remove(i)
+ }
+ }
+
+ for (i in 0..100) {
+ if (i % 2 == 0) {
+ map[i] = i.toString()
+ }
+ }
+
+ for (i in 0..100) {
+ if (i % 2 != 0) {
+ map.remove(i)
+ }
+ }
+
+ for (i in 0..100) {
+ if (i % 2 != 0) {
+ map[i] = i.toString()
+ }
+ }
+
+ assertEquals(127, map.capacity)
+ for (i in 0..100) {
+ assertTrue(map.contains(i), "Map should contain element $i")
+ }
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt
index c7879ee..721d993 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt
@@ -821,4 +821,53 @@
assertTrue(set.contains("Ciao"))
assertTrue(set.contains("Annyeong"))
}
+
+ @Test
+ fun insertOneRemoveOne() {
+ val set = MutableScatterSet<Int>()
+
+ for (i in 0..1000000) {
+ set.add(i)
+ set.remove(i)
+ assertTrue(set.capacity < 16, "Set grew larger than 16 after step $i")
+ }
+ }
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableScatterMap<Int, String>()
+
+ for (i in 0..100) {
+ map[i] = i.toString()
+ }
+
+ for (i in 0..100) {
+ if (i % 2 == 0) {
+ map.remove(i)
+ }
+ }
+
+ for (i in 0..100) {
+ if (i % 2 == 0) {
+ map[i] = i.toString()
+ }
+ }
+
+ for (i in 0..100) {
+ if (i % 2 != 0) {
+ map.remove(i)
+ }
+ }
+
+ for (i in 0..100) {
+ if (i % 2 != 0) {
+ map[i] = i.toString()
+ }
+ }
+
+ assertEquals(127, map.capacity)
+ for (i in 0..100) {
+ assertTrue(map.contains(i), "Map should contain element $i")
+ }
+ }
}
diff --git a/collection/collection/template/ObjectPValueMap.kt.template b/collection/collection/template/ObjectPValueMap.kt.template
index 1218b2d..5376d2f 100644
--- a/collection/collection/template/ObjectPValueMap.kt.template
+++ b/collection/collection/template/ObjectPValueMap.kt.template
@@ -1008,8 +1008,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -1039,6 +1038,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified
* [index]. The index must be a valid index. This function ensures the
diff --git a/collection/collection/template/ObjectPValueMapTest.kt.template b/collection/collection/template/ObjectPValueMapTest.kt.template
index f4c9fa5..988abb3 100644
--- a/collection/collection/template/ObjectPValueMapTest.kt.template
+++ b/collection/collection/template/ObjectPValueMapTest.kt.template
@@ -741,4 +741,18 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutableObjectPValueMap<Int>()
+
+ for (i in 0 .. 1000000) {
+ map[i] = i.toPValue()
+ map.remove(i)
+ assertTrue(
+ map.capacity < 16,
+ "Map grew larger than 16 after step $i"
+ )
+ }
+ }
}
diff --git a/collection/collection/template/PKeyObjectMap.kt.template b/collection/collection/template/PKeyObjectMap.kt.template
index b6740c0..5cf7ee5 100644
--- a/collection/collection/template/PKeyObjectMap.kt.template
+++ b/collection/collection/template/PKeyObjectMap.kt.template
@@ -968,8 +968,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -999,6 +998,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified
* [index]. The index must be a valid index. This function ensures the
diff --git a/collection/collection/template/PKeyObjectMapTest.kt.template b/collection/collection/template/PKeyObjectMapTest.kt.template
index 024c9d6..ea5cfb8 100644
--- a/collection/collection/template/PKeyObjectMapTest.kt.template
+++ b/collection/collection/template/PKeyObjectMapTest.kt.template
@@ -731,4 +731,18 @@
assertTrue(map.all { key, value -> key < 7KeySuffix && value.isNotEmpty() })
assertFalse(map.all { key, _ -> key < 6KeySuffix })
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutablePKeyObjectMap<String>()
+
+ for (i in 0 .. 1000000) {
+ map[i.toPKey()] = i.toString()
+ map.remove(i.toPKey())
+ assertTrue(
+ map.capacity < 16,
+ "Map grew larger than 16 after step $i"
+ )
+ }
+ }
}
diff --git a/collection/collection/template/PKeyPValueMap.kt.template b/collection/collection/template/PKeyPValueMap.kt.template
index 2da7dd1..d8bff84 100644
--- a/collection/collection/template/PKeyPValueMap.kt.template
+++ b/collection/collection/template/PKeyPValueMap.kt.template
@@ -981,8 +981,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -1012,6 +1011,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified
* [index]. The index must be a valid index. This function ensures the
diff --git a/collection/collection/template/PKeyPValueMapTest.kt.template b/collection/collection/template/PKeyPValueMapTest.kt.template
index e29c4ce..a61070f 100644
--- a/collection/collection/template/PKeyPValueMapTest.kt.template
+++ b/collection/collection/template/PKeyPValueMapTest.kt.template
@@ -716,4 +716,18 @@
assertEquals(1024, map.trim())
assertEquals(0, map.trim())
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val map = MutablePKeyPValueMap()
+
+ for (i in 0 .. 1000000) {
+ map[i.toPKey()] = i.toPValue()
+ map.remove(i.toPKey())
+ assertTrue(
+ map.capacity < 16,
+ "Map grew larger than 16 after step $i"
+ )
+ }
+ }
}
diff --git a/collection/collection/template/PKeySet.kt.template b/collection/collection/template/PKeySet.kt.template
index ddcbdba..ea71ba3 100644
--- a/collection/collection/template/PKeySet.kt.template
+++ b/collection/collection/template/PKeySet.kt.template
@@ -779,8 +779,7 @@
*/
private fun adjustStorage() {
if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
- // TODO: Avoid resize and drop deletes instead
- resizeStorage(nextCapacity(_capacity))
+ removeDeletedMarkers()
} else {
resizeStorage(nextCapacity(_capacity))
}
@@ -807,6 +806,23 @@
}
}
+ private fun removeDeletedMarkers() {
+ val m = metadata
+ val capacity = _capacity
+ var removedDeletes = 0
+
+ // TODO: this can be done in a more efficient way
+ for (i in 0 until capacity) {
+ val slot = readRawMetadata(m, i)
+ if (slot == Deleted) {
+ writeMetadata(i, Empty)
+ removedDeletes++
+ }
+ }
+
+ growthLimit += removedDeletes
+ }
+
/**
* Writes the "H2" part of an entry into the metadata array at the specified
* [index]. The index must be a valid index. This function ensures the
diff --git a/collection/collection/template/PKeySetTest.kt.template b/collection/collection/template/PKeySetTest.kt.template
index 192b395..f9b12d5 100644
--- a/collection/collection/template/PKeySetTest.kt.template
+++ b/collection/collection/template/PKeySetTest.kt.template
@@ -560,4 +560,18 @@
assertTrue(4KeySuffix in set)
assertFalse(5KeySuffix in set)
}
+
+ @Test
+ fun insertManyRemoveMany() {
+ val set = mutablePKeySetOf()
+
+ for (i in 0 .. 1000000) {
+ set.add(i.toPKey())
+ set.remove(i.toPKey())
+ assertTrue(
+ set.capacity < 16,
+ "Set grew larger than 16 after step $i"
+ )
+ }
+ }
}
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 6132c25..5c540f0 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -33,7 +33,7 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
@@ -46,7 +46,7 @@
implementation(project(":compose:ui:ui-graphics"))
implementation(project(":compose:ui:ui-util"))
implementation("androidx.collection:collection:1.4.0")
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
api(libs.kotlinCoroutinesCore)
}
}
@@ -59,8 +59,6 @@
jvmMain {
dependsOn(commonMain)
dependencies {
- implementation(libs.kotlinStdlib)
- api(libs.kotlinCoroutinesCore)
}
}
@@ -68,11 +66,10 @@
dependsOn(jvmMain)
dependencies {
api("androidx.annotation:annotation:1.1.0")
- implementation(libs.kotlinStdlib)
}
}
- desktopMain {
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
}
diff --git a/compose/animation/animation-graphics/build.gradle b/compose/animation/animation-graphics/build.gradle
index 905b5f7..f3a0352c 100644
--- a/compose/animation/animation-graphics/build.gradle
+++ b/compose/animation/animation-graphics/build.gradle
@@ -32,14 +32,14 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
commonMain {
dependencies {
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
api(project(":compose:animation:animation"))
api("androidx.compose.foundation:foundation-layout:1.6.0")
@@ -74,10 +74,9 @@
}
}
- desktopMain {
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
- implementation(libs.kotlinStdlib)
}
}
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 20406e6..6dd8e8b5 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -33,14 +33,14 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
commonMain {
dependencies {
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
api(project(":compose:animation:animation-core"))
api("androidx.compose.foundation:foundation-layout:1.6.0")
@@ -75,10 +75,9 @@
}
}
- desktopMain {
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
- implementation(libs.kotlinStdlib)
}
}
diff --git a/compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/SplineBasedDecayAnimationSpec.desktop.kt b/compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/SplineBasedDecayAnimationSpec.desktop.kt
deleted file mode 100644
index de2a36a..0000000
--- a/compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/SplineBasedDecayAnimationSpec.desktop.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.animation
-
-import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.core.generateDecayAnimationSpec
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalDensity
-
-internal actual val platformFlingScrollFriction = 0.015f
-
-@Composable
-actual fun <T> rememberSplineBasedDecay(): DecayAnimationSpec<T> {
- // This function will internally update the calculation of fling decay when the density changes,
- // but the reference to the returned spec will not change across calls.
- val density = LocalDensity.current
- return remember(density.density) {
- SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec()
- }
-}
diff --git a/compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.desktop.kt b/compose/animation/animation/src/jvmStubsMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.jvmStubs.kt
similarity index 88%
rename from compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.desktop.kt
rename to compose/animation/animation/src/jvmStubsMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.jvmStubs.kt
index f1fe969..5d4919c 100644
--- a/compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.desktop.kt
+++ b/compose/animation/animation/src/jvmStubsMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.jvmStubs.kt
@@ -20,7 +20,4 @@
import androidx.compose.runtime.Composable
@Composable
-@Deprecated("Replace with rememberSplineBasedDecay<Float>")
-actual fun defaultDecayAnimationSpec(): DecayAnimationSpec<Float> {
- return rememberSplineBasedDecay()
-}
+actual fun defaultDecayAnimationSpec(): DecayAnimationSpec<Float> = implementedInJetBrainsFork()
diff --git a/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt b/compose/animation/animation/src/jvmStubsMain/kotlin/androidx/compose/animation/NotImplemented.jvmStubs.kt
similarity index 64%
copy from compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
copy to compose/animation/animation/src/jvmStubsMain/kotlin/androidx/compose/animation/NotImplemented.jvmStubs.kt
index d4a754b..d134533 100644
--- a/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
+++ b/compose/animation/animation/src/jvmStubsMain/kotlin/androidx/compose/animation/NotImplemented.jvmStubs.kt
@@ -14,8 +14,14 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive.layout
+package androidx.compose.animation
-import androidx.compose.ui.Modifier
-
-internal actual fun Modifier.systemGestureExclusion(): Modifier = this
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun implementedInJetBrainsFork(): Nothing =
+ throw NotImplementedError(
+ """
+ Implemented only in JetBrains fork.
+ Please use `org.jetbrains.compose.animation:animation` package instead.
+ """
+ .trimIndent()
+ )
diff --git a/compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.desktop.kt b/compose/animation/animation/src/jvmStubsMain/kotlin/androidx/compose/animation/SplineBasedDecayAnimationSpec.jvmStubs.kt
similarity index 80%
copy from compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.desktop.kt
copy to compose/animation/animation/src/jvmStubsMain/kotlin/androidx/compose/animation/SplineBasedDecayAnimationSpec.jvmStubs.kt
index f1fe969..3c8f6b7 100644
--- a/compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.desktop.kt
+++ b/compose/animation/animation/src/jvmStubsMain/kotlin/androidx/compose/animation/SplineBasedDecayAnimationSpec.jvmStubs.kt
@@ -19,8 +19,7 @@
import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.runtime.Composable
+internal actual val platformFlingScrollFriction: Float = implementedInJetBrainsFork()
+
@Composable
-@Deprecated("Replace with rememberSplineBasedDecay<Float>")
-actual fun defaultDecayAnimationSpec(): DecayAnimationSpec<Float> {
- return rememberSplineBasedDecay()
-}
+actual fun <T> rememberSplineBasedDecay(): DecayAnimationSpec<T> = implementedInJetBrainsFork()
diff --git a/compose/compiler/OWNERS b/compose/compiler/OWNERS
index 51ec95d..48bdf50 100644
--- a/compose/compiler/OWNERS
+++ b/compose/compiler/OWNERS
@@ -1,5 +1,4 @@
# Bug component: 343210
[email protected]
[email protected]
[email protected]
[email protected]
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index c076bed..5019aed 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -32,15 +32,14 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
commonMain {
dependencies {
- implementation(libs.kotlinStdlibCommon)
-
+ implementation(libs.kotlinStdlib)
api("androidx.compose.ui:ui:1.6.0")
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui-util"))
@@ -70,10 +69,9 @@
}
}
- desktopMain {
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
- implementation(libs.kotlinStdlib)
}
}
diff --git a/compose/foundation/foundation-layout/src/desktopMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.desktop.kt b/compose/foundation/foundation-layout/src/desktopMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.desktop.kt
deleted file mode 100644
index b8e3738..0000000
--- a/compose/foundation/foundation-layout/src/desktopMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.desktop.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.layout
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.debugInspectorInfo
-
-actual fun Modifier.safeDrawingPadding() =
- windowInsetsPadding(debugInspectorInfo { name = "safeDrawingPadding" }) {
- WindowInsets.safeDrawing
- }
-
-actual fun Modifier.safeGesturesPadding() =
- windowInsetsPadding(debugInspectorInfo { name = "safeGesturesPadding" }) {
- WindowInsets.safeGestures
- }
-
-actual fun Modifier.safeContentPadding() =
- windowInsetsPadding(debugInspectorInfo { name = "safeContentPadding" }) {
- WindowInsets.safeContent
- }
-
-actual fun Modifier.systemBarsPadding() =
- windowInsetsPadding(debugInspectorInfo { name = "systemBarsPadding" }) {
- WindowInsets.systemBars
- }
-
-actual fun Modifier.displayCutoutPadding() =
- windowInsetsPadding(debugInspectorInfo { name = "displayCutoutPadding" }) {
- WindowInsets.displayCutout
- }
-
-actual fun Modifier.statusBarsPadding() =
- windowInsetsPadding(debugInspectorInfo { name = "statusBarsPadding" }) {
- WindowInsets.statusBars
- }
-
-actual fun Modifier.imePadding() =
- windowInsetsPadding(debugInspectorInfo { name = "imePadding" }) { WindowInsets.ime }
-
-actual fun Modifier.navigationBarsPadding() =
- windowInsetsPadding(debugInspectorInfo { name = "navigationBarsPadding" }) {
- WindowInsets.navigationBars
- }
-
-actual fun Modifier.captionBarPadding() =
- windowInsetsPadding(debugInspectorInfo { name = "captionBarPadding" }) {
- WindowInsets.captionBar
- }
-
-actual fun Modifier.waterfallPadding() =
- windowInsetsPadding(debugInspectorInfo { name = "waterfallPadding" }) { WindowInsets.waterfall }
-
-actual fun Modifier.systemGesturesPadding() =
- windowInsetsPadding(debugInspectorInfo { name = "systemGesturesPadding" }) {
- WindowInsets.systemGestures
- }
-
-actual fun Modifier.mandatorySystemGesturesPadding() =
- windowInsetsPadding(debugInspectorInfo { name = "mandatorySystemGesturesPadding" }) {
- WindowInsets.mandatorySystemGestures
- }
-
-@Suppress("NOTHING_TO_INLINE")
-@Stable
-private inline fun Modifier.windowInsetsPadding(
- noinline inspectorInfo: InspectorInfo.() -> Unit,
- crossinline insetsCalculation: @Composable () -> WindowInsets
-): Modifier =
- composed(inspectorInfo) {
- val insets = insetsCalculation()
- InsetsPaddingModifier(insets)
- }
diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt b/compose/foundation/foundation-layout/src/jvmStubsMain/kotlin/androidx/compose/foundation/layout/NotImplemented.jvmStubs.kt
similarity index 63%
rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt
rename to compose/foundation/foundation-layout/src/jvmStubsMain/kotlin/androidx/compose/foundation/layout/NotImplemented.jvmStubs.kt
index 195fdbc..612d448 100644
--- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt
+++ b/compose/foundation/foundation-layout/src/jvmStubsMain/kotlin/androidx/compose/foundation/layout/NotImplemented.jvmStubs.kt
@@ -14,15 +14,14 @@
* limitations under the License.
*/
-package androidx.compose.ui.text
+package androidx.compose.foundation.layout
-/**
- * TBD: not yet implemented.
- *
- * Converts a string with HTML tags into [AnnotatedString].
- */
-actual fun AnnotatedString.Companion.fromHtml(
- htmlString: String,
- linkStyles: TextLinkStyles?,
- linkInteractionListener: LinkInteractionListener?
-): AnnotatedString = AnnotatedString(htmlString)
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun implementedInJetBrainsFork(): Nothing =
+ throw NotImplementedError(
+ """
+ Implemented only in JetBrains fork.
+ Please use `org.jetbrains.compose.foundation:foundation-layout` package instead.
+ """
+ .trimIndent()
+ )
diff --git a/compose/foundation/foundation-layout/src/desktopMain/kotlin/androidx/compose/foundation/layout/WindowInsets.desktop.kt b/compose/foundation/foundation-layout/src/jvmStubsMain/kotlin/androidx/compose/foundation/layout/WindowInsets.jvmStubs.kt
similarity index 72%
rename from compose/foundation/foundation-layout/src/desktopMain/kotlin/androidx/compose/foundation/layout/WindowInsets.desktop.kt
rename to compose/foundation/foundation-layout/src/jvmStubsMain/kotlin/androidx/compose/foundation/layout/WindowInsets.jvmStubs.kt
index 2a17762..85096b2 100644
--- a/compose/foundation/foundation-layout/src/desktopMain/kotlin/androidx/compose/foundation/layout/WindowInsets.desktop.kt
+++ b/compose/foundation/foundation-layout/src/jvmStubsMain/kotlin/androidx/compose/foundation/layout/WindowInsets.jvmStubs.kt
@@ -16,43 +16,41 @@
package androidx.compose.foundation.layout
-private val ZeroInsets = WindowInsets(0, 0, 0, 0)
-
actual val WindowInsets.Companion.captionBar: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.displayCutout: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.ime: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.mandatorySystemGestures: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.navigationBars: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.statusBars: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.systemBars: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.systemGestures: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.tappableElement: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.waterfall: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.safeDrawing: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.safeGestures: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
actual val WindowInsets.Companion.safeContent: WindowInsets
- get() = ZeroInsets
+ get() = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation-layout/src/jvmStubsMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.jvmStubs.kt b/compose/foundation/foundation-layout/src/jvmStubsMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.jvmStubs.kt
new file mode 100644
index 0000000..ec0b474
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/jvmStubsMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.jvmStubs.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout
+
+import androidx.compose.ui.Modifier
+
+actual fun Modifier.safeDrawingPadding(): Modifier = implementedInJetBrainsFork()
+
+actual fun Modifier.safeGesturesPadding(): Modifier = implementedInJetBrainsFork()
+
+actual fun Modifier.safeContentPadding(): Modifier = implementedInJetBrainsFork()
+
+actual fun Modifier.systemBarsPadding(): Modifier = implementedInJetBrainsFork()
+
+actual fun Modifier.displayCutoutPadding(): Modifier = implementedInJetBrainsFork()
+
+actual fun Modifier.statusBarsPadding(): Modifier = implementedInJetBrainsFork()
+
+actual fun Modifier.imePadding(): Modifier = implementedInJetBrainsFork()
+
+actual fun Modifier.navigationBarsPadding(): Modifier = implementedInJetBrainsFork()
+
+actual fun Modifier.captionBarPadding(): Modifier = implementedInJetBrainsFork()
+
+actual fun Modifier.waterfallPadding(): Modifier = implementedInJetBrainsFork()
+
+actual fun Modifier.systemGesturesPadding(): Modifier = implementedInJetBrainsFork()
+
+actual fun Modifier.mandatorySystemGesturesPadding(): Modifier = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 4c2cdca..2afbfde 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -33,14 +33,14 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
commonMain {
dependencies {
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
api("androidx.collection:collection:1.4.0")
api(project(":compose:animation:animation"))
api(project(":compose:runtime:runtime"))
@@ -74,10 +74,9 @@
}
}
- desktopMain {
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
- implementation(libs.kotlinStdlib)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index a82bbc0..18065f8 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -1629,6 +1629,343 @@
}
}
+ /* Uses pointer input block for the non-dynamic pointer input and TWO pointer input
+ * blocks (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer
+ * inputs (both on same Box).
+ * Both dynamic Pointers are disabled to start and then enabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ * 3. Touch down
+ * 4. Assert
+ * 5. Touch move
+ * 6. Assert
+ * 7. Touch up
+ * 8. Assert
+ */
+ @Test
+ fun twoDynamicInputModifiers_addsAbovePointerInputWithUnitKeyTouchEventsWithMove() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputPressCounter by mutableStateOf(0)
+ var originalPointerInputMoveCounter by mutableStateOf(0)
+ var originalPointerInputReleaseCounter by mutableStateOf(0)
+
+ var activeDynamicPointerInput by mutableStateOf(false)
+ var dynamicPointerInputPressCounter by mutableStateOf(0)
+ var dynamicPointerInputMoveCounter by mutableStateOf(0)
+ var dynamicPointerInputReleaseCounter by mutableStateOf(0)
+
+ var activeDynamicPointerInput2 by mutableStateOf(false)
+ var dynamicPointerInput2PressCounter by mutableStateOf(0)
+ var dynamicPointerInput2ReleaseCounter by mutableStateOf(0)
+
+ rule.setContent {
+ Box(
+ Modifier.size(200.dp)
+ .testTag("myClickable")
+ .dynamicPointerInputModifier(
+ enabled = activeDynamicPointerInput,
+ onPress = { dynamicPointerInputPressCounter++ },
+ onMove = { dynamicPointerInputMoveCounter++ },
+ onRelease = {
+ dynamicPointerInputReleaseCounter++
+ activeDynamicPointerInput2 = true
+ }
+ )
+ .dynamicPointerInputModifier(
+ enabled = activeDynamicPointerInput2,
+ onPress = { dynamicPointerInput2PressCounter++ },
+ onRelease = { dynamicPointerInput2ReleaseCounter++ }
+ )
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Press -> {
+ originalPointerInputPressCounter++
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveCounter++
+ }
+ PointerEventType.Release -> {
+ originalPointerInputReleaseCounter++
+ activeDynamicPointerInput = true
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+
+ // Even though we are enabling the dynamic pointer input, it will NOT receive events until
+ // the next event stream (after the click is over) which is why you see zeros below.
+ // Only two events are triggered for click (down/up)
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ // With these events, we enable the dynamic pointer input
+ assertEquals(1, originalPointerInputPressCounter)
+ assertEquals(0, originalPointerInputMoveCounter)
+ assertEquals(1, originalPointerInputReleaseCounter)
+
+ assertEquals(0, dynamicPointerInputPressCounter)
+ assertEquals(0, dynamicPointerInputMoveCounter)
+ assertEquals(0, dynamicPointerInputReleaseCounter)
+
+ assertEquals(0, dynamicPointerInput2PressCounter)
+ assertEquals(0, dynamicPointerInput2ReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performTouchInput { down(Offset(0f, 0f)) }
+
+ rule.runOnIdle {
+ // Each time a new dynamic pointer input is added DIRECTLY above an existing one, the
+ // previously existing pointer input lambda will be restarted.
+ assertEquals(2, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputPressCounter)
+ assertEquals(0, originalPointerInputMoveCounter)
+ assertEquals(1, originalPointerInputReleaseCounter)
+
+ assertEquals(1, dynamicPointerInputPressCounter)
+ assertEquals(0, dynamicPointerInputMoveCounter)
+ assertEquals(0, dynamicPointerInputReleaseCounter)
+
+ assertEquals(0, dynamicPointerInput2PressCounter)
+ assertEquals(0, dynamicPointerInput2ReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performTouchInput { moveTo(Offset(1f, 1f)) }
+
+ rule.runOnIdle {
+ assertEquals(2, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputPressCounter)
+ assertEquals(1, originalPointerInputMoveCounter)
+ assertEquals(1, originalPointerInputReleaseCounter)
+
+ assertEquals(1, dynamicPointerInputPressCounter)
+ assertEquals(1, dynamicPointerInputMoveCounter)
+ assertEquals(0, dynamicPointerInputReleaseCounter)
+
+ assertEquals(0, dynamicPointerInput2PressCounter)
+ assertEquals(0, dynamicPointerInput2ReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performTouchInput { up() }
+
+ rule.runOnIdle {
+ assertEquals(2, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputPressCounter)
+ assertEquals(1, originalPointerInputMoveCounter)
+ assertEquals(2, originalPointerInputReleaseCounter)
+
+ assertEquals(1, dynamicPointerInputPressCounter)
+ assertEquals(1, dynamicPointerInputMoveCounter)
+ // With this release counter, we enable the dynamic clickable{}
+ assertEquals(1, dynamicPointerInputReleaseCounter)
+
+ assertEquals(0, dynamicPointerInput2PressCounter)
+ assertEquals(0, dynamicPointerInput2ReleaseCounter)
+ }
+
+ // Only two events are triggered for click (down/up)
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ // Each time a new dynamic pointer input is added DIRECTLY above an existing one, the
+ // previously existing pointer input lambda will be restarted.
+ assertEquals(3, originalPointerInputLambdaExecutionCount)
+ assertEquals(3, originalPointerInputPressCounter)
+ assertEquals(1, originalPointerInputMoveCounter)
+ assertEquals(3, originalPointerInputReleaseCounter)
+
+ assertEquals(2, dynamicPointerInputPressCounter)
+ assertEquals(1, dynamicPointerInputMoveCounter)
+ assertEquals(2, dynamicPointerInputReleaseCounter)
+
+ assertEquals(1, dynamicPointerInput2PressCounter)
+ assertEquals(1, dynamicPointerInput2ReleaseCounter)
+ }
+ }
+
+ /* Uses two pointer input blocks and a dynamic background color property to recompose UI.
+ * It should NOT restart the pointer input lambdas.
+ * Event sequences:
+ * 1. "click" (down/up)
+ * 2. Assert
+ * 3. Recompose
+ * 4. Assert
+ * 5. Touch down
+ * 6. Assert
+ * 7. Touch move
+ * 8. Assert
+ * 9. Touch up
+ * 10. Assert
+ * 11. Recompose
+ * 12. Assert
+ */
+ @Test
+ fun twoPointerInputModifiers_recomposeShouldNotRestartPointerInputLambda() {
+ var backgroundModifierColor by mutableStateOf(Color.Red)
+
+ var firstPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var firstPointerInputPressCounter by mutableStateOf(0)
+ var firstPointerInputMoveCounter by mutableStateOf(0)
+ var firstPointerInputReleaseCounter by mutableStateOf(0)
+
+ var secondPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var secondPointerInputPressCounter by mutableStateOf(0)
+ var secondPointerInputMoveCounter by mutableStateOf(0)
+ var secondPointerInputReleaseCounter by mutableStateOf(0)
+
+ rule.setContent {
+ Box(
+ Modifier.size(200.dp)
+ .testTag("myClickable")
+ .pointerInput(Unit) {
+ firstPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Press -> {
+ firstPointerInputPressCounter++
+ }
+ PointerEventType.Move -> {
+ firstPointerInputMoveCounter++
+ }
+ PointerEventType.Release -> {
+ firstPointerInputReleaseCounter++
+ }
+ }
+ }
+ }
+ }
+ .pointerInput(Unit) {
+ secondPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Press -> {
+ secondPointerInputPressCounter++
+ }
+ PointerEventType.Move -> {
+ secondPointerInputMoveCounter++
+ }
+ PointerEventType.Release -> {
+ secondPointerInputReleaseCounter++
+ }
+ }
+ }
+ }
+ }
+ .background(backgroundModifierColor)
+ )
+ }
+
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertEquals(Color.Red, backgroundModifierColor)
+
+ assertEquals(1, firstPointerInputLambdaExecutionCount)
+ assertEquals(1, firstPointerInputPressCounter)
+ assertEquals(0, firstPointerInputMoveCounter)
+ assertEquals(1, firstPointerInputReleaseCounter)
+
+ assertEquals(1, secondPointerInputLambdaExecutionCount)
+ assertEquals(1, secondPointerInputPressCounter)
+ assertEquals(0, secondPointerInputMoveCounter)
+ assertEquals(1, secondPointerInputReleaseCounter)
+ }
+
+ // Recompose
+ backgroundModifierColor = Color.Green
+
+ rule.runOnIdle {
+ assertEquals(Color.Green, backgroundModifierColor)
+
+ assertEquals(1, firstPointerInputLambdaExecutionCount)
+ assertEquals(1, firstPointerInputPressCounter)
+ assertEquals(0, firstPointerInputMoveCounter)
+ assertEquals(1, firstPointerInputReleaseCounter)
+
+ assertEquals(1, secondPointerInputLambdaExecutionCount)
+ assertEquals(1, secondPointerInputPressCounter)
+ assertEquals(0, secondPointerInputMoveCounter)
+ assertEquals(1, secondPointerInputReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performTouchInput { down(Offset(0f, 0f)) }
+
+ rule.runOnIdle {
+ assertEquals(Color.Green, backgroundModifierColor)
+
+ assertEquals(1, firstPointerInputLambdaExecutionCount)
+ assertEquals(2, firstPointerInputPressCounter)
+ assertEquals(0, firstPointerInputMoveCounter)
+ assertEquals(1, firstPointerInputReleaseCounter)
+
+ assertEquals(1, secondPointerInputLambdaExecutionCount)
+ assertEquals(2, secondPointerInputPressCounter)
+ assertEquals(0, secondPointerInputMoveCounter)
+ assertEquals(1, secondPointerInputReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performTouchInput { moveTo(Offset(1f, 1f)) }
+
+ rule.runOnIdle {
+ assertEquals(Color.Green, backgroundModifierColor)
+
+ assertEquals(1, firstPointerInputLambdaExecutionCount)
+ assertEquals(2, firstPointerInputPressCounter)
+ assertEquals(1, firstPointerInputMoveCounter)
+ assertEquals(1, firstPointerInputReleaseCounter)
+
+ assertEquals(1, secondPointerInputLambdaExecutionCount)
+ assertEquals(2, secondPointerInputPressCounter)
+ assertEquals(1, secondPointerInputMoveCounter)
+ assertEquals(1, secondPointerInputReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performTouchInput { up() }
+
+ rule.runOnIdle {
+ assertEquals(Color.Green, backgroundModifierColor)
+
+ assertEquals(1, firstPointerInputLambdaExecutionCount)
+ assertEquals(2, firstPointerInputPressCounter)
+ assertEquals(1, firstPointerInputMoveCounter)
+ assertEquals(2, firstPointerInputReleaseCounter)
+
+ assertEquals(1, secondPointerInputLambdaExecutionCount)
+ assertEquals(2, secondPointerInputPressCounter)
+ assertEquals(1, secondPointerInputMoveCounter)
+ assertEquals(2, secondPointerInputReleaseCounter)
+ }
+
+ // Recompose
+ backgroundModifierColor = Color.Red
+
+ rule.runOnIdle {
+ assertEquals(Color.Red, backgroundModifierColor)
+
+ assertEquals(1, firstPointerInputLambdaExecutionCount)
+ assertEquals(2, firstPointerInputPressCounter)
+ assertEquals(1, firstPointerInputMoveCounter)
+ assertEquals(2, firstPointerInputReleaseCounter)
+
+ assertEquals(1, secondPointerInputLambdaExecutionCount)
+ assertEquals(2, secondPointerInputPressCounter)
+ assertEquals(1, secondPointerInputMoveCounter)
+ assertEquals(2, secondPointerInputReleaseCounter)
+ }
+ }
+
/* Uses pointer input block for the non-dynamic pointer input and BOTH a clickable{} and
* pointer input block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer
* inputs (both on same Box).
@@ -1654,7 +1991,9 @@
var dynamicPointerInputReleaseCounter by mutableStateOf(0)
var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
- var originalPointerInputEventCounter by mutableStateOf(0)
+ var originalPointerInputPressCounter by mutableStateOf(0)
+ var originalPointerInputMoveCounter by mutableStateOf(0)
+ var originalPointerInputReleaseCounter by mutableStateOf(0)
rule.setContent {
Box(
@@ -1670,19 +2009,23 @@
}
)
.dynamicClickableModifier(activeDynamicClickable) { dynamicClickableCounter++ }
- // Note the .background() above the static pointer input block
- // TODO (jjw): Remove once bug fixed for when a dynamic pointer input follows
- // directly after another pointer input (both using Unit key).
- // Workaround: add a modifier between them OR use unique keys (that is, not
- // Unit)
- .background(Color.Green)
.pointerInput(Unit) {
originalPointerInputLambdaExecutionCount++
awaitPointerEventScope {
while (true) {
- awaitPointerEvent()
- originalPointerInputEventCounter++
- activeDynamicPointerInput = true
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Press -> {
+ originalPointerInputPressCounter++
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveCounter++
+ }
+ PointerEventType.Release -> {
+ originalPointerInputReleaseCounter++
+ activeDynamicPointerInput = true
+ }
+ }
}
}
}
@@ -1697,7 +2040,9 @@
rule.runOnIdle {
assertEquals(1, originalPointerInputLambdaExecutionCount)
// With these events, we enable the dynamic pointer input
- assertEquals(2, originalPointerInputEventCounter)
+ assertEquals(1, originalPointerInputPressCounter)
+ assertEquals(0, originalPointerInputMoveCounter)
+ assertEquals(1, originalPointerInputReleaseCounter)
assertEquals(0, dynamicPointerInputPressCounter)
assertEquals(0, dynamicPointerInputMoveCounter)
@@ -1709,8 +2054,12 @@
rule.onNodeWithTag("myClickable").performTouchInput { down(Offset(0f, 0f)) }
rule.runOnIdle {
- assertEquals(1, originalPointerInputLambdaExecutionCount)
- assertEquals(3, originalPointerInputEventCounter)
+ // Each time a new dynamic pointer input is added DIRECTLY above an existing one, the
+ // previously existing pointer input lambda will be restarted.
+ assertEquals(2, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputPressCounter)
+ assertEquals(0, originalPointerInputMoveCounter)
+ assertEquals(1, originalPointerInputReleaseCounter)
assertEquals(1, dynamicPointerInputPressCounter)
assertEquals(0, dynamicPointerInputMoveCounter)
@@ -1722,8 +2071,10 @@
rule.onNodeWithTag("myClickable").performTouchInput { moveTo(Offset(1f, 1f)) }
rule.runOnIdle {
- assertEquals(1, originalPointerInputLambdaExecutionCount)
- assertEquals(4, originalPointerInputEventCounter)
+ assertEquals(2, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputPressCounter)
+ assertEquals(1, originalPointerInputMoveCounter)
+ assertEquals(1, originalPointerInputReleaseCounter)
assertEquals(1, dynamicPointerInputPressCounter)
assertEquals(1, dynamicPointerInputMoveCounter)
@@ -1735,8 +2086,10 @@
rule.onNodeWithTag("myClickable").performTouchInput { up() }
rule.runOnIdle {
- assertEquals(1, originalPointerInputLambdaExecutionCount)
- assertEquals(5, originalPointerInputEventCounter)
+ assertEquals(2, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputPressCounter)
+ assertEquals(1, originalPointerInputMoveCounter)
+ assertEquals(2, originalPointerInputReleaseCounter)
assertEquals(1, dynamicPointerInputPressCounter)
assertEquals(1, dynamicPointerInputMoveCounter)
@@ -1750,8 +2103,12 @@
rule.onNodeWithTag("myClickable").performClick()
rule.runOnIdle {
- assertEquals(1, originalPointerInputLambdaExecutionCount)
- assertEquals(7, originalPointerInputEventCounter)
+ // In this case, the lambda is not re-executed because the modifier added before it was
+ // not directly a pointer input.
+ assertEquals(2, originalPointerInputLambdaExecutionCount)
+ assertEquals(3, originalPointerInputPressCounter)
+ assertEquals(1, originalPointerInputMoveCounter)
+ assertEquals(3, originalPointerInputReleaseCounter)
assertEquals(2, dynamicPointerInputPressCounter)
assertEquals(1, dynamicPointerInputMoveCounter)
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehaviorTest.kt
index f9f245d..c7f63dc 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehaviorTest.kt
@@ -181,6 +181,43 @@
}
@Test
+ fun performFling_invalidOffsets_shouldNotPropagateNans_calculateSnapOffset() {
+ val testLayoutInfoProvider =
+ object : SnapLayoutInfoProvider {
+ override fun calculateSnapOffset(velocity: Float): Float = Float.NaN
+ }
+ lateinit var testFlingBehavior: TargetedFlingBehavior
+ val exception =
+ kotlin.runCatching {
+ rule.setContent {
+ testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+ VelocityEffect(testFlingBehavior, TestVelocity)
+ }
+ }
+ assert(exception.isFailure)
+ }
+
+ @Test
+ fun performFling_invalidOffsets_shouldNotPropagateNans_calculateApproachOffset() {
+ val testLayoutInfoProvider =
+ object : SnapLayoutInfoProvider {
+ override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float =
+ Float.NaN
+
+ override fun calculateSnapOffset(velocity: Float): Float = 0.0f
+ }
+ lateinit var testFlingBehavior: TargetedFlingBehavior
+ val exception =
+ kotlin.runCatching {
+ rule.setContent {
+ testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+ VelocityEffect(testFlingBehavior, TestVelocity)
+ }
+ }
+ assert(exception.isFailure)
+ }
+
+ @Test
fun findClosestOffset_noFlingDirection_shouldReturnAbsoluteDistance() {
val testLayoutInfoProvider = TestLayoutInfoProvider()
val offset = testLayoutInfoProvider.calculateSnapOffset(0f)
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
index 14858a4..18afddd 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
@@ -541,8 +541,8 @@
inputMethodInterceptor.withInputConnection { secondInputConnection = this }
}
- // check that input connection was restarted after window focus toggle
- assertThat(firstInputConnection).isNotSameInstanceAs(secondInputConnection)
+ // check that we have not created a separate input connection
+ assertThat(firstInputConnection).isSameInstanceAs(secondInputConnection)
}
private fun setContent(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index d7ed4fc..5ddc1e0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -1310,7 +1310,7 @@
* updated ro the [targetValue] without updating the offset.
*
* @param targetValue The target value of the animation
- * @param velocity The velocity the animation should start with
+ * @param velocity The velocity the animation should start with, in px/s
* @param snapAnimationSpec The animation spec used if the velocity is not high enough to perform a
* decay to the [targetValue] using the [decayAnimationSpec]
* @param decayAnimationSpec The animation spec used if the velocity is high enough to perform a
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/PagerSnapLayoutInfoProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/PagerSnapLayoutInfoProvider.kt
index deac658..1f7188c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/PagerSnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/PagerSnapLayoutInfoProvider.kt
@@ -69,6 +69,9 @@
debugLog { "Approach Velocity=$velocity" }
val effectivePageSizePx = pagerState.pageSize + pagerState.pageSpacing
+ // Page Size is Zero, do not proceed.
+ if (effectivePageSizePx == 0) return 0f
+
// given this velocity, where can I go with a decay animation.
val animationOffsetPx = decayOffset
@@ -244,8 +247,13 @@
"layoutDirection=$layoutDirection"
}
// how many pages have I scrolled using a drag gesture.
+ val pageSize = pagerState.layoutInfo.pageSize
val offsetFromSnappedPosition =
- pagerState.dragGestureDelta() / pagerState.layoutInfo.pageSize.toFloat()
+ if (pageSize == 0) {
+ 0f
+ } else {
+ pagerState.dragGestureDelta() / pageSize.toFloat()
+ }
// we're only interested in the decimal part of the offset.
val offsetFromSnappedPositionOverflow =
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
index 134e790..e4e0ac9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
@@ -117,8 +117,13 @@
val initialOffset =
snapLayoutInfoProvider.calculateApproachOffset(initialVelocity, decayOffset)
- var remainingScrollOffset =
- abs(initialOffset) * sign(initialVelocity) // ensure offset sign is correct
+
+ check(!initialOffset.isNaN()) {
+ "calculateApproachOffset returned NaN. Please use a valid value."
+ }
+
+ // ensure offset sign and value are correct
+ var remainingScrollOffset = abs(initialOffset) * sign(initialVelocity)
onRemainingScrollOffsetUpdate(remainingScrollOffset) // First Scroll Offset
@@ -128,9 +133,15 @@
onRemainingScrollOffsetUpdate(remainingScrollOffset)
}
- remainingScrollOffset =
+ val finalSnapOffset =
snapLayoutInfoProvider.calculateSnapOffset(animationState.velocity)
+ check(!finalSnapOffset.isNaN()) {
+ "calculateSnapOffset returned NaN. Please use a valid value."
+ }
+
+ remainingScrollOffset = finalSnapOffset
+
debugLog { "Settling Final Bound=$remainingScrollOffset" }
animateWithTarget(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 0877475..6b6deb5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -346,19 +346,13 @@
// Hide the keyboard if made disabled or read-only while focused (b/237308379).
val writeable by rememberUpdatedState(enabled && !readOnly)
- val isWindowFocused by rememberUpdatedState(windowInfo.isWindowFocused)
LaunchedEffect(Unit) {
try {
- snapshotFlow {
- // do not use Pair to pass two booleans
- packBools(writeable, isWindowFocused)
- }
- .collect {
- @Suppress("NAME_SHADOWING") val writeable = unpackBool1(it)
- @Suppress("NAME_SHADOWING") val isWindowFocused = unpackBool2(it)
+ snapshotFlow { writeable }
+ .collect { writeable ->
// When hasFocus changes, the session will be stopped/started in the focus
// handler so we don't need to handle its changes here.
- if (writeable && isWindowFocused && state.hasFocus) {
+ if (writeable && state.hasFocus) {
startInputSession(
textInputService,
state,
@@ -366,7 +360,7 @@
imeOptions,
manager.offsetMapping
)
- } else if (!writeable || !state.hasFocus) {
+ } else {
endInputSession(state)
}
}
@@ -1260,15 +1254,3 @@
)
}
}
-
-private fun packBools(bool1: Boolean, bool2: Boolean): Int {
- return (if (bool1) (0x1) else 0x0) or (if (bool2) (0x2) else 0x0)
-}
-
-private fun unpackBool1(packed: Int): Boolean {
- return (packed and 0x1) > 0
-}
-
-private fun unpackBool2(packed: Int): Boolean {
- return (packed and 0x2) > 0
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
index a04a59b..cd38a53 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
@@ -355,7 +355,12 @@
private var windowInfo: WindowInfo? = null
private val isFocused: Boolean
- get() = isElementFocused && windowInfo?.isWindowFocused == true
+ get() {
+ // make sure that we read both window focus and element focus for snapshot aware
+ // callers to successfully update when either one changes
+ val isWindowFocused = windowInfo?.isWindowFocused == true
+ return isElementFocused && isWindowFocused
+ }
/**
* We observe text changes to show/hide text toolbar and cursor handles. This job is only run
@@ -678,7 +683,6 @@
observeReads {
windowInfo = currentValueOf(LocalWindowInfo)
onFocusChange()
- startInputSessionOnWindowFocusChange()
}
}
@@ -712,15 +716,6 @@
stylusHandwritingTrigger?.resetReplayCache()
}
- private fun startInputSessionOnWindowFocusChange() {
- if (windowInfo == null) return
- // b/326323000: We do not dispose input session on just window focus change until another
- // item requests focus and we lose element focus status which is handled by onFocusEvent.
- if (windowInfo?.isWindowFocused == true && isElementFocused) {
- startInputSession(fromTap = false)
- }
- }
-
private fun requireKeyboardController(): SoftwareKeyboardController =
currentValueOf(LocalSoftwareKeyboardController) ?: error("No software keyboard controller")
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.desktop.kt
deleted file mode 100644
index cdc9fb2..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.desktop.kt
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Popup
-import androidx.compose.ui.window.PopupProperties
-import androidx.compose.ui.window.rememberCursorPositionProvider
-
-// Design of basic represenation is from Material specs:
-// https://material.io/design/interaction/states.html#hover
-// https://material.io/components/menus#specs
-
-val LightDefaultContextMenuRepresentation =
- DefaultContextMenuRepresentation(
- backgroundColor = Color.White,
- textColor = Color.Black,
- itemHoverColor = Color.Black.copy(alpha = 0.04f)
- )
-
-val DarkDefaultContextMenuRepresentation =
- DefaultContextMenuRepresentation(
- backgroundColor = Color(0xFF121212), // like surface in darkColors
- textColor = Color.White,
- itemHoverColor = Color.White.copy(alpha = 0.04f)
- )
-
-class DefaultContextMenuRepresentation(
- private val backgroundColor: Color,
- private val textColor: Color,
- private val itemHoverColor: Color
-) : ContextMenuRepresentation {
- @Composable
- override fun Representation(state: ContextMenuState, items: List<ContextMenuItem>) {
- val isOpen = state.status is ContextMenuState.Status.Open
- if (isOpen) {
- Popup(
- popupPositionProvider = rememberCursorPositionProvider(),
- onDismissRequest = { state.status = ContextMenuState.Status.Closed },
- properties = PopupProperties(focusable = true)
- ) {
- Column(
- modifier =
- Modifier.shadow(8.dp)
- .background(backgroundColor)
- .padding(vertical = 4.dp)
- .width(IntrinsicSize.Max)
- .verticalScroll(rememberScrollState())
- ) {
- items
- .distinctBy { it.label }
- .forEach { item ->
- MenuItemContent(
- itemHoverColor = itemHoverColor,
- onClick = {
- state.status = ContextMenuState.Status.Closed
- item.onClick()
- }
- ) {
- BasicText(text = item.label, style = TextStyle(color = textColor))
- }
- }
- }
- }
- }
- }
-}
-
-@Composable
-private fun MenuItemContent(
- itemHoverColor: Color,
- onClick: () -> Unit,
- content: @Composable RowScope.() -> Unit
-) {
- var hovered by remember { mutableStateOf(false) }
- Row(
- modifier =
- Modifier.clickable(
- onClick = onClick,
- )
- .onHover { hovered = it }
- .background(if (hovered) itemHoverColor else Color.Transparent)
- .fillMaxWidth()
- // Preferred min and max width used during the intrinsic measurement.
- .sizeIn(minWidth = 112.dp, maxWidth = 280.dp, minHeight = 32.dp)
- .padding(PaddingValues(horizontal = 16.dp, vertical = 0.dp)),
- verticalAlignment = Alignment.CenterVertically
- ) {
- content()
- }
-}
-
-private fun Modifier.onHover(onHover: (Boolean) -> Unit) =
- pointerInput(Unit) {
- awaitPointerEventScope {
- while (true) {
- val event = awaitPointerEvent()
- when (event.type) {
- PointerEventType.Enter -> onHover(true)
- PointerEventType.Exit -> onHover(false)
- }
- }
- }
- }
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicTooltip.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicTooltip.desktop.kt
deleted file mode 100644
index e906226..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicTooltip.desktop.kt
+++ /dev/null
@@ -1,74 +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.compose.foundation
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.window.Popup
-import androidx.compose.ui.window.PopupPositionProvider
-import androidx.compose.ui.window.PopupProperties
-
-/**
- * BasicTooltipBox that wraps a composable with a tooltip.
- *
- * Tooltip that provides a descriptive message for an anchor. It can be used to call the users
- * attention to the anchor.
- *
- * @param positionProvider [PopupPositionProvider] that will be used to place the tooltip relative
- * to the anchor content.
- * @param tooltip the composable that will be used to populate the tooltip's content.
- * @param state handles the state of the tooltip's visibility.
- * @param modifier the [Modifier] to be applied to this BasicTooltipBox.
- * @param focusable [Boolean] that determines if the tooltip is focusable. When true, the tooltip
- * will consume touch events while it's shown and will have accessibility focus move to the first
- * element of the component. When false, the tooltip won't consume touch events while it's shown
- * but assistive-tech users will need to swipe or drag to get to the first element of the
- * component.
- * @param enableUserInput [Boolean] which determines if this BasicTooltipBox will handle long press
- * and mouse hover to trigger the tooltip through the state provided.
- * @param content the composable that the tooltip will anchor to.
- */
-@Composable
-actual fun BasicTooltipBox(
- positionProvider: PopupPositionProvider,
- tooltip: @Composable () -> Unit,
- state: BasicTooltipState,
- modifier: Modifier,
- focusable: Boolean,
- enableUserInput: Boolean,
- content: @Composable () -> Unit
-) {
- // TODO: Reuse android implementation - there is no platform specifics here.
- // Use expect/actual only for string resources
- Box(modifier = modifier) {
- content()
- if (state.isVisible) {
- Popup(
- popupPositionProvider = positionProvider,
- onDismissRequest = { state.dismiss() },
- properties =
- PopupProperties(
- // TODO(b/326167778): focusable = true cannot work with mouse
- focusable = false
- )
- ) {
- tooltip()
- }
- }
- }
-}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
deleted file mode 100644
index b5e7252..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation
-
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
-import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.nativeKeyCode
-import androidx.compose.ui.input.key.type
-import androidx.compose.ui.node.DelegatableNode
-import java.awt.event.KeyEvent.VK_ENTER
-
-internal actual fun DelegatableNode.isComposeRootInScrollableContainer(): Boolean {
- return false
-}
-
-// TODO: b/168524931 - should this depend on the input device?
-internal actual val TapIndicationDelay: Long = 0L
-
-/**
- * Whether the specified [KeyEvent] should trigger a press for a clickable component, i.e. whether
- * it is associated with a press of the enter key.
- */
-internal actual val KeyEvent.isPress: Boolean
- get() = type == KeyDown && key.nativeKeyCode == VK_ENTER
-
-/**
- * Whether the specified [KeyEvent] should trigger a click for a clickable component, i.e. whether
- * it is associated with a release of the enter key.
- */
-internal actual val KeyEvent.isClick: Boolean
- get() = type == KeyUp && key.nativeKeyCode == VK_ENTER
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ContextMenuProvider.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ContextMenuProvider.desktop.kt
deleted file mode 100644
index e38f648..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ContextMenuProvider.desktop.kt
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation
-
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.ProvidableCompositionLocal
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.changedToDown
-import androidx.compose.ui.input.pointer.isSecondaryPressed
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.util.fastAll
-
-/**
- * Defines a container where context menu is available. Menu is triggered by right mouse clicks.
- * Representation of menu is defined by [LocalContextMenuRepresentation]`
- *
- * @param items List of context menu items. Final context menu contains all items from descendant
- * [ContextMenuArea] and [ContextMenuDataProvider].
- * @param state [ContextMenuState] of menu controlled by this area.
- * @param enabled If false then gesture detector is disabled.
- * @param content The content of the [ContextMenuArea].
- */
-@Composable
-fun ContextMenuArea(
- items: () -> List<ContextMenuItem>,
- state: ContextMenuState = remember { ContextMenuState() },
- enabled: Boolean = true,
- content: @Composable () -> Unit
-) {
- val data = ContextMenuData(items, LocalContextMenuData.current)
-
- ContextMenuDataProvider(data) {
- Box(Modifier.contextMenuDetector(state, enabled), propagateMinConstraints = true) {
- content()
- }
- LocalContextMenuRepresentation.current.Representation(state, data.allItems)
- }
-}
-
-/**
- * Adds items to the hierarchy of context menu items. Can be used, for example, to customize context
- * menu of text fields.
- *
- * @param items List of context menu items. Final context menu contains all items from descendant
- * [ContextMenuArea] and [ContextMenuDataProvider].
- * @param content The content of the [ContextMenuDataProvider].
- * @see [[ContextMenuArea]]
- */
-@Composable
-fun ContextMenuDataProvider(items: () -> List<ContextMenuItem>, content: @Composable () -> Unit) {
- ContextMenuDataProvider(ContextMenuData(items, LocalContextMenuData.current), content)
-}
-
-@Composable
-internal fun ContextMenuDataProvider(data: ContextMenuData, content: @Composable () -> Unit) {
- CompositionLocalProvider(LocalContextMenuData provides data) { content() }
-}
-
-private val LocalContextMenuData = staticCompositionLocalOf<ContextMenuData?> { null }
-
-private fun Modifier.contextMenuDetector(
- state: ContextMenuState,
- enabled: Boolean = true
-): Modifier {
- return if (enabled && state.status == ContextMenuState.Status.Closed) {
- this.pointerInput(state) {
- awaitEachGesture {
- val event = awaitEventFirstDown()
- if (event.buttons.isSecondaryPressed) {
- event.changes.forEach { it.consume() }
- state.status = ContextMenuState.Status.Open(Rect(event.changes[0].position, 0f))
- }
- }
- }
- } else {
- Modifier
- }
-}
-
-private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent {
- var event: PointerEvent
- do {
- event = awaitPointerEvent()
- } while (!event.changes.fastAll { it.changedToDown() })
- return event
-}
-
-/**
- * Individual element of context menu.
- *
- * @param label The text to be displayed as a context menu item.
- * @param onClick The action to be executed after click on the item.
- */
-class ContextMenuItem(val label: String, val onClick: () -> Unit) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as ContextMenuItem
-
- if (label != other.label) return false
- if (onClick != other.onClick) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = label.hashCode()
- result = 31 * result + onClick.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "ContextMenuItem(label='$label')"
- }
-}
-
-/**
- * Data container contains all [ContextMenuItem]s were defined previously in the hierarchy.
- * [ContextMenuRepresentation] uses it to display context menu.
- */
-class ContextMenuData(val items: () -> List<ContextMenuItem>, val next: ContextMenuData?) {
-
- internal val allItems: List<ContextMenuItem> by lazy { allItemsSeq.toList() }
-
- internal val allItemsSeq: Sequence<ContextMenuItem>
- get() = sequence {
- yieldAll(items())
- next?.let { yieldAll(it.allItemsSeq) }
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as ContextMenuData
-
- if (items != other.items) return false
- if (next != other.next) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = items.hashCode()
- result = 31 * result + (next?.hashCode() ?: 0)
- return result
- }
-}
-
-/**
- * Represents a state of context menu in [ContextMenuArea]. [status] is implemented via
- * [androidx.compose.runtime.MutableState] so it's possible to track it inside @Composable
- * functions.
- */
-class ContextMenuState {
- sealed class Status {
- class Open(val rect: Rect) : Status() {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as Open
-
- if (rect != other.rect) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- return rect.hashCode()
- }
-
- override fun toString(): String {
- return "Open(rect=$rect)"
- }
- }
-
- object Closed : Status()
- }
-
- var status: Status by mutableStateOf(Status.Closed)
-}
-
-/**
- * Implementations of this interface are responsible for displaying context menus. There are two
- * implementations out of the box: [LightDefaultContextMenuRepresentation] and
- * [DarkDefaultContextMenuRepresentation]. To change currently used representation, different value
- * for [LocalContextMenuRepresentation] could be provided.
- */
-interface ContextMenuRepresentation {
- @Composable fun Representation(state: ContextMenuState, items: List<ContextMenuItem>)
-}
-
-/** Composition local that keeps [ContextMenuRepresentation] which is used by [ContextMenuArea]s. */
-val LocalContextMenuRepresentation: ProvidableCompositionLocal<ContextMenuRepresentation> =
- staticCompositionLocalOf {
- LightDefaultContextMenuRepresentation
- }
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DarkTheme.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DarkTheme.desktop.kt
deleted file mode 100644
index 7021d2e..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DarkTheme.desktop.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
-import org.jetbrains.skiko.SystemTheme
-import org.jetbrains.skiko.currentSystemTheme
-
-/**
- * This function should be used to help build responsive UIs that follow the system setting, to
- * avoid harsh contrast changes when switching between applications.
- *
- * This function returns `true` if the [Configuration.UI_MODE_NIGHT_YES] bit is set. It is also
- * possible for this bit to be [Configuration.UI_MODE_NIGHT_UNDEFINED], in which case light theme is
- * treated as the default, and this function returns `false`.
- *
- * It is also recommended to provide user accessible overrides in your application, so users can
- * choose to force an always-light or always-dark theme. To do this, you should provide the current
- * theme value in a CompositionLocal or similar to components further down your hierarchy, only
- * calling this effect once at the top level if no user override has been set. This also helps avoid
- * multiple calls to this effect, which can be expensive as it queries system configuration.
- *
- * For example, to draw a white rectangle when in dark theme, and a black rectangle when in light
- * theme:
- *
- * @sample androidx.compose.foundation.samples.DarkThemeSample
- * @return `true` if the system is considered to be in 'dark theme'.
- */
-@Composable
-@ReadOnlyComposable
-internal actual fun _isSystemInDarkTheme(): Boolean {
- return currentSystemTheme == SystemTheme.DARK
-}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopPlatform.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopPlatform.desktop.kt
deleted file mode 100644
index 15969ef..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopPlatform.desktop.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation
-
-internal enum class DesktopPlatform {
- Linux,
- Windows,
- MacOS,
- Unknown;
-
- companion object {
- /** Identify OS on which the application is currently running. */
- val Current: DesktopPlatform by lazy {
- val name = System.getProperty("os.name")
- when {
- name?.startsWith("Linux") == true -> Linux
- name?.startsWith("Win") == true -> Windows
- name == "Mac OS X" -> MacOS
- else -> Unknown
- }
- }
- }
-}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt
deleted file mode 100644
index 95b8e38..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt
+++ /dev/null
@@ -1,577 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation
-
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.TweenSpec
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.detectTapAndPress
-import androidx.compose.foundation.gestures.drag
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.interaction.DragInteraction
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.collectIsHoveredAsState
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocal
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.MeasurePolicy
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.constrainHeight
-import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.unit.dp
-import kotlin.math.abs
-import kotlin.math.roundToInt
-import kotlin.math.sign
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.runBlocking
-
-/**
- * [CompositionLocal] used to pass [ScrollbarStyle] down the tree. This value is typically set in
- * some "Theme" composable function (DesktopTheme, MaterialTheme)
- */
-val LocalScrollbarStyle = staticCompositionLocalOf { defaultScrollbarStyle() }
-
-/**
- * Defines visual style of scrollbars (thickness, shapes, colors, etc). Can be passed as a parameter
- * of scrollbar through [LocalScrollbarStyle]
- */
-@Immutable
-data class ScrollbarStyle(
- val minimalHeight: Dp,
- val thickness: Dp,
- val shape: Shape,
- val hoverDurationMillis: Int,
- val unhoverColor: Color,
- val hoverColor: Color
-)
-
-/** Simple default [ScrollbarStyle] without applying MaterialTheme. */
-fun defaultScrollbarStyle() =
- ScrollbarStyle(
- minimalHeight = 16.dp,
- thickness = 8.dp,
- shape = RoundedCornerShape(4.dp),
- hoverDurationMillis = 300,
- unhoverColor = Color.Black.copy(alpha = 0.12f),
- hoverColor = Color.Black.copy(alpha = 0.50f)
- )
-
-/**
- * Vertical scrollbar that can be attached to some scrollable component (ScrollableColumn,
- * LazyColumn) and share common state with it.
- *
- * Can be placed independently.
- *
- * Example: val state = rememberScrollState(0f)
- *
- * Box(Modifier.fillMaxSize()) {
- * Box(modifier = Modifier.verticalScroll(state)) {
- * ...
- * }
- * VerticalScrollbar(
- * Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
- * rememberScrollbarAdapter(state)
- * )
- * }
- *
- * @param adapter [ScrollbarAdapter] that will be used to communicate with scrollable component
- * @param modifier the modifier to apply to this layout
- * @param reverseLayout reverse the direction of scrolling and layout, when `true` and
- * [LazyListState.firstVisibleItemIndex] == 0 then scrollbar will be at the bottom of the
- * container. It is usually used in pair with `LazyColumn(reverseLayout = true)`
- * @param style [ScrollbarStyle] to define visual style of scrollbar
- * @param interactionSource optional [MutableInteractionSource] that will be used to dispatch
- * [DragInteraction.Start] when this Scrollbar is being dragged.
- */
-@Composable
-fun VerticalScrollbar(
- adapter: ScrollbarAdapter,
- modifier: Modifier = Modifier,
- reverseLayout: Boolean = false,
- style: ScrollbarStyle = LocalScrollbarStyle.current,
- interactionSource: MutableInteractionSource? = null
-) = Scrollbar(adapter, modifier, reverseLayout, style, interactionSource, isVertical = true)
-
-/**
- * Horizontal scrollbar that can be attached to some scrollable component
- * (Modifier.verticalScroll(), LazyRow) and share common state with it.
- *
- * Can be placed independently.
- *
- * Example: val state = rememberScrollState(0f)
- *
- * Box(Modifier.fillMaxSize()) {
- * Box(modifier = Modifier.verticalScroll(state)) {
- * ...
- * }
- * HorizontalScrollbar(
- * Modifier.align(Alignment.BottomCenter).fillMaxWidth(),
- * rememberScrollbarAdapter(state)
- * )
- * }
- *
- * @param adapter [ScrollbarAdapter] that will be used to communicate with scrollable component
- * @param modifier the modifier to apply to this layout
- * @param reverseLayout reverse the direction of scrolling and layout, when `true` and
- * [LazyListState.firstVisibleItemIndex] == 0 then scrollbar will be at the end of the container.
- * It is usually used in pair with `LazyRow(reverseLayout = true)`
- * @param style [ScrollbarStyle] to define visual style of scrollbar
- * @param interactionSource optional [MutableInteractionSource] that will be used to dispatch
- * [DragInteraction.Start] when this Scrollbar is being dragged.
- */
-@Composable
-fun HorizontalScrollbar(
- adapter: ScrollbarAdapter,
- modifier: Modifier = Modifier,
- reverseLayout: Boolean = false,
- style: ScrollbarStyle = LocalScrollbarStyle.current,
- interactionSource: MutableInteractionSource? = null
-) =
- Scrollbar(
- adapter,
- modifier,
- if (LocalLayoutDirection.current == LayoutDirection.Rtl) !reverseLayout else reverseLayout,
- style,
- interactionSource,
- isVertical = false
- )
-
-// TODO(demin): do we need to stop dragging if cursor is beyond constraints?
-@Composable
-private fun Scrollbar(
- adapter: ScrollbarAdapter,
- modifier: Modifier = Modifier,
- reverseLayout: Boolean,
- style: ScrollbarStyle,
- interactionSource: MutableInteractionSource?,
- isVertical: Boolean
-) =
- with(LocalDensity.current) {
- @Suppress("NAME_SHADOWING")
- val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
- val dragInteraction = remember { mutableStateOf<DragInteraction.Start?>(null) }
- DisposableEffect(interactionSource) {
- onDispose {
- dragInteraction.value?.let { interaction ->
- interactionSource.tryEmit(DragInteraction.Cancel(interaction))
- dragInteraction.value = null
- }
- }
- }
-
- var containerSize by remember { mutableStateOf(0) }
- val isHovered by interactionSource.collectIsHoveredAsState()
-
- val isHighlighted by remember {
- derivedStateOf { isHovered || dragInteraction.value is DragInteraction.Start }
- }
-
- val minimalHeight = style.minimalHeight.toPx()
- val sliderAdapter =
- remember(adapter, containerSize, minimalHeight, reverseLayout) {
- SliderAdapter(adapter, containerSize, minimalHeight, reverseLayout)
- }
-
- val scrollThickness = style.thickness.roundToPx()
- val measurePolicy =
- if (isVertical) {
- remember(sliderAdapter, scrollThickness) {
- verticalMeasurePolicy(sliderAdapter, { containerSize = it }, scrollThickness)
- }
- } else {
- remember(sliderAdapter, scrollThickness) {
- horizontalMeasurePolicy(sliderAdapter, { containerSize = it }, scrollThickness)
- }
- }
-
- val color by
- animateColorAsState(
- if (isHighlighted) style.hoverColor else style.unhoverColor,
- animationSpec = TweenSpec(durationMillis = style.hoverDurationMillis)
- )
-
- val isVisible = sliderAdapter.size < containerSize
-
- Layout(
- {
- Box(
- Modifier.background(if (isVisible) color else Color.Transparent, style.shape)
- .scrollbarDrag(
- interactionSource = interactionSource,
- draggedInteraction = dragInteraction,
- onDelta = { offset ->
- sliderAdapter.rawPosition += if (isVertical) offset.y else offset.x
- },
- onFinished = { sliderAdapter.rawPosition = sliderAdapter.position }
- )
- )
- },
- modifier
- .hoverable(interactionSource = interactionSource)
- .scrollOnPressOutsideSlider(isVertical, sliderAdapter, adapter, containerSize),
- measurePolicy
- )
- }
-
-private fun Modifier.scrollbarDrag(
- interactionSource: MutableInteractionSource,
- draggedInteraction: MutableState<DragInteraction.Start?>,
- onDelta: (Offset) -> Unit,
- onFinished: () -> Unit
-): Modifier = composed {
- val currentInteractionSource by rememberUpdatedState(interactionSource)
- val currentDraggedInteraction by rememberUpdatedState(draggedInteraction)
- val currentOnDelta by rememberUpdatedState(onDelta)
- val currentOnFinished by rememberUpdatedState(onFinished)
- pointerInput(Unit) {
- awaitEachGesture {
- val down = awaitFirstDown(requireUnconsumed = false)
- val interaction = DragInteraction.Start()
- currentInteractionSource.tryEmit(interaction)
- currentDraggedInteraction.value = interaction
- val isSuccess =
- drag(down.id) { change ->
- currentOnDelta.invoke(change.positionChange())
- change.consume()
- }
- val finishInteraction =
- if (isSuccess) {
- DragInteraction.Stop(interaction)
- } else {
- DragInteraction.Cancel(interaction)
- }
- currentInteractionSource.tryEmit(finishInteraction)
- currentDraggedInteraction.value = null
- currentOnFinished.invoke()
- }
- }
-}
-
-private fun Modifier.scrollOnPressOutsideSlider(
- isVertical: Boolean,
- sliderAdapter: SliderAdapter,
- scrollbarAdapter: ScrollbarAdapter,
- containerSize: Int
-) = composed {
- var targetOffset: Offset? by remember { mutableStateOf(null) }
-
- if (targetOffset != null) {
- val targetPosition = if (isVertical) targetOffset!!.y else targetOffset!!.x
-
- LaunchedEffect(targetPosition) {
- var delay = PressTimeoutMillis * 3
- while (targetPosition !in sliderAdapter.bounds) {
- val oldSign = sign(targetPosition - sliderAdapter.position)
- scrollbarAdapter.scrollTo(
- containerSize,
- scrollbarAdapter.scrollOffset + oldSign * containerSize
- )
- val newSign = sign(targetPosition - sliderAdapter.position)
-
- if (oldSign != newSign) {
- break
- }
-
- delay(delay)
- delay = PressTimeoutMillis
- }
- }
- }
- Modifier.pointerInput(Unit) {
- detectTapAndPress(
- onPress = { offset ->
- targetOffset = offset
- tryAwaitRelease()
- targetOffset = null
- },
- onTap = {}
- )
- }
-}
-
-/**
- * Create and [remember] [ScrollbarAdapter] for scrollable container and current instance of
- * [scrollState]
- */
-@Composable
-fun rememberScrollbarAdapter(scrollState: ScrollState): ScrollbarAdapter =
- remember(scrollState) { ScrollbarAdapter(scrollState) }
-
-/**
- * Create and [remember] [ScrollbarAdapter] for lazy scrollable container and current instance of
- * [scrollState]
- */
-@Composable
-fun rememberScrollbarAdapter(
- scrollState: LazyListState,
-): ScrollbarAdapter {
- return remember(scrollState) { ScrollbarAdapter(scrollState) }
-}
-
-/**
- * ScrollbarAdapter for Modifier.verticalScroll and Modifier.horizontalScroll
- *
- * [scrollState] is instance of [ScrollState] which is used by scrollable component
- *
- * Example: val state = rememberScrollState(0f)
- *
- * Box(Modifier.fillMaxSize()) {
- * Box(modifier = Modifier.verticalScroll(state)) {
- * ...
- * }
- * VerticalScrollbar(
- * Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
- * rememberScrollbarAdapter(state)
- * )
- * }
- */
-fun ScrollbarAdapter(scrollState: ScrollState): ScrollbarAdapter =
- ScrollableScrollbarAdapter(scrollState)
-
-private class ScrollableScrollbarAdapter(private val scrollState: ScrollState) : ScrollbarAdapter {
- override val scrollOffset: Float
- get() = scrollState.value.toFloat()
-
- override suspend fun scrollTo(containerSize: Int, scrollOffset: Float) {
- scrollState.scrollTo(scrollOffset.roundToInt())
- }
-
- override fun maxScrollOffset(containerSize: Int) = scrollState.maxValue.toFloat()
-}
-
-/**
- * ScrollbarAdapter for lazy lists.
- *
- * [scrollState] is instance of [LazyListState] which is used by scrollable component
- *
- * Scrollbar size and position will be dynamically changed on the current visible content.
- *
- * Example: Box(Modifier.fillMaxSize()) { val state = rememberLazyListState()
- *
- * LazyColumn(state = state) {
- * ...
- * }
- * VerticalScrollbar(
- * Modifier.align(Alignment.CenterEnd),
- * rememberScrollbarAdapter(state)
- * )
- * }
- */
-fun ScrollbarAdapter(scrollState: LazyListState): ScrollbarAdapter =
- LazyScrollbarAdapter(scrollState)
-
-private class LazyScrollbarAdapter(private val scrollState: LazyListState) : ScrollbarAdapter {
- override val scrollOffset: Float
- get() =
- scrollState.firstVisibleItemIndex * averageItemSize +
- scrollState.firstVisibleItemScrollOffset
-
- override suspend fun scrollTo(containerSize: Int, scrollOffset: Float) {
- val distance = scrollOffset - [email protected]
-
- // if we scroll less than containerSize we need to use scrollBy function to avoid
- // undesirable scroll jumps (when an item size is different)
- //
- // if we scroll more than containerSize we should immediately jump to this position
- // without recreating all items between the current and the new position
- if (abs(distance) <= containerSize) {
- scrollState.scrollBy(distance)
- } else {
- snapTo(containerSize, scrollOffset)
- }
- }
-
- private suspend fun snapTo(containerSize: Int, scrollOffset: Float) {
- // In case of very big values, we can catch an overflow, so convert values to double and
- // coerce them
- // val averageItemSize = 26.000002f
- // val scrollOffsetCoerced = 2.54490608E8.toFloat()
- // val index = (scrollOffsetCoerced / averageItemSize).toInt() // 9788100
- // val offset = (scrollOffsetCoerced - index * averageItemSize) // -16.0
- // println(offset)
-
- val maximumValue = maxScrollOffset(containerSize).toDouble()
- val scrollOffsetCoerced = scrollOffset.toDouble().coerceIn(0.0, maximumValue)
- val averageItemSize = averageItemSize.toDouble()
-
- val index =
- (scrollOffsetCoerced / averageItemSize)
- .toInt()
- .coerceAtLeast(0)
- .coerceAtMost(itemCount - 1)
-
- val offset = (scrollOffsetCoerced - index * averageItemSize).toInt().coerceAtLeast(0)
-
- scrollState.scrollToItem(index = index, scrollOffset = offset)
- }
-
- override fun maxScrollOffset(containerSize: Int) =
- (averageItemSize * itemCount - containerSize).coerceAtLeast(0f)
-
- private val itemCount
- get() = scrollState.layoutInfo.totalItemsCount
-
- private val averageItemSize by derivedStateOf {
- scrollState.layoutInfo.visibleItemsInfo.asSequence().map { it.size }.average().toFloat()
- }
-}
-
-/** Defines how to scroll the scrollable component */
-interface ScrollbarAdapter {
- /**
- * Scroll offset of the content inside the scrollable component. Offset "100" means that the
- * content is scrolled by 100 pixels from the start.
- */
- val scrollOffset: Float
-
- /**
- * Instantly jump to [scrollOffset] in pixels
- *
- * @param containerSize size of the scrollable container (for example, it is height of
- * ScrollableColumn if we use VerticalScrollbar)
- * @param scrollOffset target value in pixels to jump to, value will be coerced to
- * 0..maxScrollOffset
- */
- suspend fun scrollTo(containerSize: Int, scrollOffset: Float)
-
- /**
- * Maximum scroll offset of the content inside the scrollable component
- *
- * @param containerSize size of the scrollable component (for example, it is height of
- * ScrollableColumn if we use VerticalScrollbar)
- */
- fun maxScrollOffset(containerSize: Int): Float
-}
-
-private class SliderAdapter(
- val adapter: ScrollbarAdapter,
- val containerSize: Int,
- val minHeight: Float,
- val reverseLayout: Boolean
-) {
- private val contentSize
- get() = adapter.maxScrollOffset(containerSize) + containerSize
-
- private val visiblePart
- get() = containerSize.toFloat() / contentSize
-
- val size
- get() =
- (containerSize * visiblePart)
- .coerceAtLeast(minHeight)
- .coerceAtMost(containerSize.toFloat())
-
- private val scrollScale: Float
- get() {
- val extraScrollbarSpace = containerSize - size
- val extraContentSpace = contentSize - containerSize
- return if (extraContentSpace == 0f) 1f else extraScrollbarSpace / extraContentSpace
- }
-
- /** A position with cumulative offset, may be out of the container when dragging */
- var rawPosition: Float = position
- set(value) {
- field = value
- position = value
- }
-
- /** Actual scroll of content regarding slider layout */
- private var scrollPosition: Float
- get() = scrollScale * adapter.scrollOffset
- set(value) {
- runBlocking { adapter.scrollTo(containerSize, value / scrollScale) }
- }
-
- /** Actual position of a thumb within slider container */
- var position: Float
- get() = if (reverseLayout) containerSize - size - scrollPosition else scrollPosition
- set(value) {
- scrollPosition =
- if (reverseLayout) {
- containerSize - size - value
- } else {
- value
- }
- }
-
- val bounds
- get() = position..position + size
-}
-
-private fun verticalMeasurePolicy(
- sliderAdapter: SliderAdapter,
- setContainerSize: (Int) -> Unit,
- scrollThickness: Int
-) = MeasurePolicy { measurables, constraints ->
- setContainerSize(constraints.maxHeight)
- val height = sliderAdapter.size.toInt()
- val placeable =
- measurables
- .first()
- .measure(Constraints.fixed(constraints.constrainWidth(scrollThickness), height))
- layout(placeable.width, constraints.maxHeight) {
- placeable.place(0, sliderAdapter.position.toInt())
- }
-}
-
-private fun horizontalMeasurePolicy(
- sliderAdapter: SliderAdapter,
- setContainerSize: (Int) -> Unit,
- scrollThickness: Int
-) = MeasurePolicy { measurables, constraints ->
- setContainerSize(constraints.maxWidth)
- val width = sliderAdapter.size.toInt()
- val placeable =
- measurables
- .first()
- .measure(Constraints.fixed(width, constraints.constrainHeight(scrollThickness)))
- layout(constraints.maxWidth, placeable.height) {
- placeable.place(sliderAdapter.position.toInt(), 0)
- }
-}
-
-/**
- * The time that must elapse before a tap gesture sends onTapDown, if there's any doubt that the
- * gesture is a tap.
- */
-private const val PressTimeoutMillis: Long = 100L
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/TooltipArea.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/TooltipArea.desktop.kt
deleted file mode 100644
index cb4460ec..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/TooltipArea.desktop.kt
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.changedToDown
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.positionInWindow
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.DpOffset
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntRect
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.PopupPositionProvider
-import androidx.compose.ui.window.rememberComponentRectPositionProvider
-import androidx.compose.ui.window.rememberCursorPositionProvider
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-/**
- * Sets the tooltip for an element.
- *
- * @param tooltip Composable content of the tooltip.
- * @param modifier The modifier to be applied to the layout.
- * @param contentAlignment The default alignment inside the Box.
- * @param propagateMinConstraints Whether the incoming min constraints should be passed to content.
- * @param delay Delay in milliseconds.
- * @param tooltipPlacement Defines position of the tooltip.
- * @param content Composable content that the current tooltip is set to.
- */
-@Composable
-@Deprecated(
- "Use TooltipArea",
- replaceWith = ReplaceWith("TooltipArea(tooltip, modifier, delay, tooltipPlacement, content)")
-)
-@Suppress("UNUSED_PARAMETER")
-fun BoxWithTooltip(
- tooltip: @Composable () -> Unit,
- modifier: Modifier = Modifier,
- contentAlignment: Alignment = Alignment.TopStart,
- propagateMinConstraints: Boolean = false,
- delay: Int = 500,
- tooltipPlacement: TooltipPlacement =
- TooltipPlacement.CursorPoint(offset = DpOffset(0.dp, 16.dp)),
- content: @Composable () -> Unit
-) = TooltipArea(tooltip, modifier, delay, tooltipPlacement, content)
-
-/**
- * Sets the tooltip for an element.
- *
- * @param tooltip Composable content of the tooltip.
- * @param modifier The modifier to be applied to the layout.
- * @param delayMillis Delay in milliseconds.
- * @param tooltipPlacement Defines position of the tooltip.
- * @param content Composable content that the current tooltip is set to.
- */
-@Composable
-fun TooltipArea(
- tooltip: @Composable () -> Unit,
- modifier: Modifier = Modifier,
- delayMillis: Int = 500,
- tooltipPlacement: TooltipPlacement =
- TooltipPlacement.CursorPoint(offset = DpOffset(0.dp, 16.dp)),
- content: @Composable () -> Unit
-) {
- val mousePosition = remember { mutableStateOf(IntOffset.Zero) }
- var parentBounds by remember { mutableStateOf(IntRect.Zero) }
- val state = rememberBasicTooltipState(initialIsVisible = false)
- val scope = rememberCoroutineScope()
- var job: Job? by remember { mutableStateOf(null) }
-
- fun startShowing() {
- job?.cancel()
- job =
- scope.launch {
- delay(delayMillis.toLong())
- state.show()
- }
- }
-
- fun hide() {
- job?.cancel()
- state.dismiss()
- }
-
- BasicTooltipBox(
- positionProvider = tooltipPlacement.positionProvider(),
- tooltip = tooltip,
- modifier =
- modifier
- .onGloballyPositioned { coordinates ->
- val size = coordinates.size
- val position =
- IntOffset(
- coordinates.positionInWindow().x.toInt(),
- coordinates.positionInWindow().y.toInt()
- )
- parentBounds = IntRect(position, size)
- }
- /** TODO: b/296850580 Figure out touch input story for desktop */
- .pointerInput(Unit) {
- awaitPointerEventScope {
- while (true) {
- val event = awaitPointerEvent()
- val position = event.changes.first().position
- when (event.type) {
- PointerEventType.Move -> {
- mousePosition.value =
- IntOffset(
- position.x.toInt() + parentBounds.left,
- position.y.toInt() + parentBounds.top
- )
- }
- PointerEventType.Enter -> {
- startShowing()
- }
- PointerEventType.Exit -> {
- hide()
- }
- }
- }
- }
- }
- .pointerInput(Unit) { detectDown { hide() } },
- focusable = false,
- enableUserInput = true,
- state = state,
- content = content
- )
-}
-
-private suspend fun PointerInputScope.detectDown(onDown: (Offset) -> Unit) {
- while (true) {
- awaitPointerEventScope {
- val event = awaitPointerEvent(PointerEventPass.Initial)
- val down = event.changes.find { it.changedToDown() }
- if (down != null) {
- onDown(down.position)
- }
- }
- }
-}
-
-/** An interface for providing a [PopupPositionProvider] for the tooltip. */
-interface TooltipPlacement {
- /** Returns [PopupPositionProvider] implementation. */
- @Composable fun positionProvider(): PopupPositionProvider
-
- /**
- * [TooltipPlacement] implementation for providing a [PopupPositionProvider] that calculates the
- * position of the popup relative to the current mouse cursor position.
- *
- * @param offset [DpOffset] to be added to the position of the popup.
- * @param alignment The alignment of the popup relative to the current cursor position.
- * @param windowMargin Defines the area within the window that limits the placement of the
- * popup.
- */
- class CursorPoint(
- private val offset: DpOffset = DpOffset.Zero,
- private val alignment: Alignment = Alignment.BottomEnd,
- private val windowMargin: Dp = 4.dp
- ) : TooltipPlacement {
- @Composable
- override fun positionProvider() =
- rememberCursorPositionProvider(offset, alignment, windowMargin)
- }
-
- /**
- * [TooltipPlacement] implementation for providing a [PopupPositionProvider] that calculates the
- * position of the popup relative to the current component bounds.
- *
- * @param anchor The anchor point relative to the current component bounds.
- * @param alignment The alignment of the popup relative to the [anchor] point.
- * @param offset [DpOffset] to be added to the position of the popup.
- */
- class ComponentRect(
- private val anchor: Alignment = Alignment.BottomCenter,
- private val alignment: Alignment = Alignment.BottomCenter,
- private val offset: DpOffset = DpOffset.Zero
- ) : TooltipPlacement {
- @Composable
- override fun positionProvider() =
- rememberComponentRectPositionProvider(anchor, alignment, offset)
- }
-}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/DesktopScrollable.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/DesktopScrollable.desktop.kt
deleted file mode 100644
index ec05c06..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/DesktopScrollable.desktop.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.gestures
-
-import androidx.compose.foundation.DesktopPlatform
-import androidx.compose.runtime.compositionLocalOf
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
-import androidx.compose.ui.node.currentValueOf
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastFold
-import java.awt.event.MouseWheelEvent
-import kotlin.math.sqrt
-
-// TODO(demin): Chrome on Windows/Linux uses different scroll strategy
-// (always the same scroll offset, bounds-independent).
-// Figure out why and decide if we can use this strategy instead of the current one.
-internal val LocalScrollConfig = compositionLocalOf {
- when (DesktopPlatform.Current) {
- DesktopPlatform.Linux -> LinuxGnomeConfig
- DesktopPlatform.Windows -> WindowsWinUIConfig
- DesktopPlatform.MacOS -> MacOSCocoaConfig
- DesktopPlatform.Unknown -> WindowsWinUIConfig
- }
-}
-
-internal actual fun CompositionLocalConsumerModifierNode.platformScrollConfig() =
- currentValueOf(LocalScrollConfig)
-
-// TODO(demin): is this formula actually correct? some experimental values don't fit
-// the formula
-internal object LinuxGnomeConfig : ScrollConfig {
- // the formula was determined experimentally based on Ubuntu Nautilus behaviour
- override fun Density.calculateMouseWheelScroll(event: PointerEvent, bounds: IntSize): Offset {
- return if (event.shouldScrollByPage) {
- calculateOffsetByPage(event, bounds)
- } else {
- Offset(
- x = event.totalScrollDelta.x * sqrt(bounds.width.toFloat()),
- y = event.totalScrollDelta.y * sqrt(bounds.height.toFloat())
- )
- } * -event.scrollAmount
- }
-}
-
-internal object WindowsWinUIConfig : ScrollConfig {
- // the formula was determined experimentally based on Windows Start behaviour
- override fun Density.calculateMouseWheelScroll(event: PointerEvent, bounds: IntSize): Offset {
- return if (event.shouldScrollByPage) {
- calculateOffsetByPage(event, bounds)
- } else {
- Offset(
- x = event.totalScrollDelta.x * (bounds.width / 20f),
- y = event.totalScrollDelta.y * (bounds.height / 20f)
- )
- } * -event.scrollAmount
- }
-}
-
-internal object MacOSCocoaConfig : ScrollConfig {
- // the formula was determined experimentally based on MacOS Finder behaviour
- // MacOS driver will send events with accelerating delta
- override fun Density.calculateMouseWheelScroll(event: PointerEvent, bounds: IntSize): Offset {
- return if (event.shouldScrollByPage) {
- calculateOffsetByPage(event, bounds)
- } else {
- event.totalScrollDelta * 10.dp.toPx()
- } * -event.scrollAmount
- }
-}
-
-// TODO(demin): Chrome/Firefox on Windows scroll differently: value * 0.90f * bounds
-// the formula was determined experimentally based on Windows Start behaviour
-private fun calculateOffsetByPage(event: PointerEvent, bounds: IntSize): Offset {
- return Offset(
- x = event.totalScrollDelta.x * bounds.width,
- y = event.totalScrollDelta.y * bounds.height
- )
-}
-
-private val PointerEvent.scrollAmount
- get() = (mouseEvent as? MouseWheelEvent)?.scrollAmount?.toFloat() ?: 1f
-
-private val PointerEvent.shouldScrollByPage
- get() = (mouseEvent as? MouseWheelEvent)?.scrollType == MouseWheelEvent.WHEEL_BLOCK_SCROLL
-
-private val PointerEvent.totalScrollDelta
- get() = this.changes.fastFold(Offset.Zero) { acc, c -> acc + c.scrollDelta }
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/ContextMenu.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/ContextMenu.desktop.kt
deleted file mode 100644
index 0ea9b78..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/ContextMenu.desktop.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text
-
-import androidx.compose.foundation.ContextMenuArea
-import androidx.compose.foundation.ContextMenuItem
-import androidx.compose.foundation.ContextMenuState
-import androidx.compose.foundation.DesktopPlatform
-import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState
-import androidx.compose.foundation.text.selection.SelectionManager
-import androidx.compose.foundation.text.selection.TextFieldSelectionManager
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.platform.LocalLocalization
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-
-@Composable
-internal actual fun ContextMenuArea(
- manager: TextFieldSelectionManager,
- content: @Composable () -> Unit
-) {
- val state = remember { ContextMenuState() }
- if (DesktopPlatform.Current == DesktopPlatform.MacOS) {
- OpenMenuAdjuster(state) { manager.contextMenuOpenAdjustment(it) }
- }
- ContextMenuArea(manager.contextMenuItems(), state, content = content)
-}
-
-// todo implement
-@Composable
-internal actual inline fun ContextMenuArea(
- selectionState: TextFieldSelectionState,
- enabled: Boolean,
- content: @Composable () -> Unit
-) {
- content()
-}
-
-@Composable
-internal actual fun ContextMenuArea(manager: SelectionManager, content: @Composable () -> Unit) {
- val state = remember { ContextMenuState() }
- if (DesktopPlatform.Current == DesktopPlatform.MacOS) {
- OpenMenuAdjuster(state) { manager.contextMenuOpenAdjustment(it) }
- }
- ContextMenuArea(manager.contextMenuItems(), state, content = content)
-}
-
-@Composable
-internal fun OpenMenuAdjuster(state: ContextMenuState, adjustAction: (Offset) -> Unit) {
- LaunchedEffect(state) {
- snapshotFlow { state.status }
- .collect { status ->
- if (status is ContextMenuState.Status.Open) {
- adjustAction(status.rect.center)
- }
- }
- }
-}
-
-@Composable
-internal fun TextFieldSelectionManager.contextMenuItems(): () -> List<ContextMenuItem> {
- val platformLocalization = LocalLocalization.current
- return {
- val result = mutableListOf<ContextMenuItem>()
- val isPassword = visualTransformation is PasswordVisualTransformation
- if (!value.selection.collapsed && !isPassword) {
- result.add(
- ContextMenuItem(platformLocalization.copy) {
- copy(false)
- focusRequester?.requestFocus()
- }
- )
- }
-
- if (!value.selection.collapsed && editable && !isPassword) {
- result.add(
- ContextMenuItem(platformLocalization.cut) {
- cut()
- focusRequester?.requestFocus()
- }
- )
- }
-
- if (editable && clipboardManager?.getText() != null) {
- result.add(
- ContextMenuItem(platformLocalization.paste) {
- paste()
- focusRequester?.requestFocus()
- }
- )
- }
-
- if (value.selection.length != value.text.length) {
- result.add(
- ContextMenuItem(platformLocalization.selectAll) {
- selectAll()
- focusRequester?.requestFocus()
- }
- )
- }
- result
- }
-}
-
-@Composable
-internal fun SelectionManager.contextMenuItems(): () -> List<ContextMenuItem> {
- val localization = LocalLocalization.current
- return { listOf(ContextMenuItem(localization.copy) { copy() }) }
-}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyMapping.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyMapping.desktop.kt
deleted file mode 100644
index 19b8e73..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyMapping.desktop.kt
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text
-
-import androidx.compose.foundation.DesktopPlatform
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.isAltPressed
-import androidx.compose.ui.input.key.isCtrlPressed
-import androidx.compose.ui.input.key.isMetaPressed
-import androidx.compose.ui.input.key.isShiftPressed
-import androidx.compose.ui.input.key.key
-import java.awt.event.KeyEvent as AwtKeyEvent
-
-internal actual val platformDefaultKeyMapping: KeyMapping =
- when (DesktopPlatform.Current) {
- DesktopPlatform.MacOS -> {
- val common = commonKeyMapping(KeyEvent::isMetaPressed)
- object : KeyMapping {
- override fun map(event: KeyEvent): KeyCommand? {
- return when {
- event.isMetaPressed && event.isCtrlPressed ->
- when (event.key) {
- MappedKeys.Space -> KeyCommand.CHARACTER_PALETTE
- else -> null
- }
- event.isShiftPressed && event.isAltPressed ->
- when (event.key) {
- MappedKeys.DirectionLeft -> KeyCommand.SELECT_LEFT_WORD
- MappedKeys.DirectionRight -> KeyCommand.SELECT_RIGHT_WORD
- MappedKeys.DirectionUp -> KeyCommand.SELECT_PREV_PARAGRAPH
- MappedKeys.DirectionDown -> KeyCommand.SELECT_NEXT_PARAGRAPH
- else -> null
- }
- event.isShiftPressed && event.isMetaPressed ->
- when (event.key) {
- MappedKeys.DirectionLeft -> KeyCommand.SELECT_LINE_LEFT
- MappedKeys.DirectionRight -> KeyCommand.SELECT_LINE_RIGHT
- MappedKeys.DirectionUp -> KeyCommand.SELECT_HOME
- MappedKeys.DirectionDown -> KeyCommand.SELECT_END
- else -> null
- }
- event.isMetaPressed ->
- when (event.key) {
- MappedKeys.DirectionLeft -> KeyCommand.LINE_LEFT
- MappedKeys.DirectionRight -> KeyCommand.LINE_RIGHT
- MappedKeys.DirectionUp -> KeyCommand.HOME
- MappedKeys.DirectionDown -> KeyCommand.END
- MappedKeys.Backspace -> KeyCommand.DELETE_FROM_LINE_START
- else -> null
- }
-
- // Emacs-like shortcuts
- event.isCtrlPressed && event.isShiftPressed && event.isAltPressed -> {
- when (event.key) {
- MappedKeys.F -> KeyCommand.SELECT_RIGHT_WORD
- MappedKeys.B -> KeyCommand.SELECT_LEFT_WORD
- else -> null
- }
- }
- event.isCtrlPressed && event.isAltPressed -> {
- when (event.key) {
- MappedKeys.F -> KeyCommand.RIGHT_WORD
- MappedKeys.B -> KeyCommand.LEFT_WORD
- else -> null
- }
- }
- event.isCtrlPressed && event.isShiftPressed -> {
- when (event.key) {
- MappedKeys.F -> KeyCommand.SELECT_RIGHT_CHAR
- MappedKeys.B -> KeyCommand.SELECT_LEFT_CHAR
- MappedKeys.P -> KeyCommand.SELECT_UP
- MappedKeys.N -> KeyCommand.SELECT_DOWN
- MappedKeys.A -> KeyCommand.SELECT_LINE_START
- MappedKeys.E -> KeyCommand.SELECT_LINE_END
- else -> null
- }
- }
- event.isCtrlPressed -> {
- when (event.key) {
- MappedKeys.F -> KeyCommand.LEFT_CHAR
- MappedKeys.B -> KeyCommand.RIGHT_CHAR
- MappedKeys.P -> KeyCommand.UP
- MappedKeys.N -> KeyCommand.DOWN
- MappedKeys.A -> KeyCommand.LINE_START
- MappedKeys.E -> KeyCommand.LINE_END
- MappedKeys.H -> KeyCommand.DELETE_PREV_CHAR
- MappedKeys.D -> KeyCommand.DELETE_NEXT_CHAR
- MappedKeys.K -> KeyCommand.DELETE_TO_LINE_END
- MappedKeys.O -> KeyCommand.NEW_LINE
- else -> null
- }
- }
- // end of emacs-like shortcuts
-
- event.isShiftPressed ->
- when (event.key) {
- MappedKeys.MoveHome -> KeyCommand.SELECT_HOME
- MappedKeys.MoveEnd -> KeyCommand.SELECT_END
- else -> null
- }
- event.isAltPressed ->
- when (event.key) {
- MappedKeys.DirectionLeft -> KeyCommand.LEFT_WORD
- MappedKeys.DirectionRight -> KeyCommand.RIGHT_WORD
- MappedKeys.DirectionUp -> KeyCommand.PREV_PARAGRAPH
- MappedKeys.DirectionDown -> KeyCommand.NEXT_PARAGRAPH
- MappedKeys.Delete -> KeyCommand.DELETE_NEXT_WORD
- MappedKeys.Backspace -> KeyCommand.DELETE_PREV_WORD
- else -> null
- }
- else -> null
- } ?: common.map(event)
- }
- }
- }
- else -> defaultKeyMapping
- }
-
-internal actual object MappedKeys {
- actual val A: Key = Key(AwtKeyEvent.VK_A)
- val B: Key = Key(AwtKeyEvent.VK_B)
- val D: Key = Key(AwtKeyEvent.VK_D)
- actual val C: Key = Key(AwtKeyEvent.VK_C)
- val E: Key = Key(AwtKeyEvent.VK_E)
- val F: Key = Key(AwtKeyEvent.VK_F)
- actual val H: Key = Key(AwtKeyEvent.VK_H)
- val K: Key = Key(AwtKeyEvent.VK_K)
- val N: Key = Key(AwtKeyEvent.VK_N)
- val O: Key = Key(AwtKeyEvent.VK_O)
- val P: Key = Key(AwtKeyEvent.VK_P)
- actual val V: Key = Key(AwtKeyEvent.VK_V)
- actual val X: Key = Key(AwtKeyEvent.VK_X)
- actual val Y: Key = Key(AwtKeyEvent.VK_Y)
- actual val Z: Key = Key(AwtKeyEvent.VK_Z)
- actual val Backslash: Key = Key(AwtKeyEvent.VK_BACK_SLASH)
- actual val DirectionLeft: Key = Key(AwtKeyEvent.VK_LEFT)
- actual val DirectionRight: Key = Key(AwtKeyEvent.VK_RIGHT)
- actual val DirectionUp: Key = Key(AwtKeyEvent.VK_UP)
- actual val DirectionDown: Key = Key(AwtKeyEvent.VK_DOWN)
- actual val PageUp: Key = Key(AwtKeyEvent.VK_PAGE_UP)
- actual val PageDown: Key = Key(AwtKeyEvent.VK_PAGE_DOWN)
- actual val MoveHome: Key = Key(AwtKeyEvent.VK_HOME)
- actual val MoveEnd: Key = Key(AwtKeyEvent.VK_END)
- actual val Insert: Key = Key(AwtKeyEvent.VK_INSERT)
- actual val Enter: Key = Key(AwtKeyEvent.VK_ENTER)
- actual val Backspace: Key = Key(AwtKeyEvent.VK_BACK_SPACE)
- actual val Delete: Key = Key(AwtKeyEvent.VK_DELETE)
- actual val Paste: Key = Key(AwtKeyEvent.VK_PASTE)
- actual val Cut: Key = Key(AwtKeyEvent.VK_CUT)
- actual val Copy: Key = Key(AwtKeyEvent.VK_COPY)
- actual val Tab: Key = Key(AwtKeyEvent.VK_TAB)
- val Space: Key = Key(AwtKeyEvent.VK_SPACE)
-}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/StringHelpers.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/StringHelpers.desktop.kt
deleted file mode 100644
index 43c84e4..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/StringHelpers.desktop.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text
-
-import org.jetbrains.skia.BreakIterator
-
-internal actual fun String.findPrecedingBreak(index: Int): Int {
- val it = BreakIterator.makeCharacterInstance()
- it.setText(this)
- return it.preceding(index)
-}
-
-internal actual fun String.findFollowingBreak(index: Int): Int {
- val it = BreakIterator.makeCharacterInstance()
- it.setText(this)
- return it.following(index)
-}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextFieldFocusModifier.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextFieldFocusModifier.desktop.kt
deleted file mode 100644
index 2c336bbe..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextFieldFocusModifier.desktop.kt
+++ /dev/null
@@ -1,33 +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.compose.foundation.text
-
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusManager
-
-/**
- * TextField consumes the D-pad keys, due to which we can't move focus once a TextField is focused.
- * To prevent this, this modifier can be used to intercept D-pad key events before they are sent to
- * the TextField. It intercepts and handles the directional (Up, Down, Left, Right & Center) D-pad
- * key presses, to move the focus between TextField and other focusable items on the screen.
- *
- * TODO: To be implemented if there's any specific handling required for desktop platform
- */
-internal actual fun Modifier.interceptDPadAndMoveFocus(
- state: LegacyTextFieldState,
- focusManager: FocusManager
-): Modifier = this
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.desktop.kt
deleted file mode 100644
index c56f4b3..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.desktop.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text
-
-import androidx.compose.ui.input.key.KeyEvent
-
-private fun Char.isPrintable(): Boolean {
- val block = Character.UnicodeBlock.of(this)
- return (!Character.isISOControl(this)) &&
- this != java.awt.event.KeyEvent.CHAR_UNDEFINED &&
- block != null &&
- block != Character.UnicodeBlock.SPECIALS
-}
-
-actual val KeyEvent.isTypedEvent: Boolean
- get() =
- nativeKeyEvent.id == java.awt.event.KeyEvent.KEY_TYPED &&
- nativeKeyEvent.keyChar.isPrintable()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.desktop.kt
deleted file mode 100644
index db9a9b9..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.desktop.kt
+++ /dev/null
@@ -1,259 +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.compose.foundation.text.input.internal
-
-import androidx.compose.foundation.text.input.internal.DesktopLegacyPlatformTextInputServiceAdapter.CurrentInput
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.platform.PlatformTextInputMethodRequest
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.input.CommitTextCommand
-import androidx.compose.ui.text.input.DeleteSurroundingTextInCodePointsCommand
-import androidx.compose.ui.text.input.EditCommand
-import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.text.input.ImeOptions
-import androidx.compose.ui.text.input.SetComposingTextCommand
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.text.substring
-import java.awt.Rectangle
-import java.awt.event.InputMethodEvent
-import java.awt.event.InputMethodListener
-import java.awt.font.TextHitInfo
-import java.awt.im.InputMethodRequests
-import java.text.AttributedCharacterIterator
-import java.text.AttributedString
-import java.text.CharacterIterator
-import java.util.Locale
-import kotlin.math.max
-import kotlin.math.min
-import kotlinx.coroutines.Job
-
-internal actual fun createLegacyPlatformTextInputServiceAdapter():
- LegacyPlatformTextInputServiceAdapter = DesktopLegacyPlatformTextInputServiceAdapter()
-
-internal class DesktopLegacyPlatformTextInputServiceAdapter :
- LegacyPlatformTextInputServiceAdapter() {
-
- data class CurrentInput(
- var value: TextFieldValue,
- val onEditCommand: ((List<EditCommand>) -> Unit),
- val onImeActionPerformed: ((ImeAction) -> Unit),
- val imeAction: ImeAction,
- var focusedRect: Rect? = null
- )
-
- private var job: Job? = null
- private var currentInput: CurrentInput? = null
-
- override fun startInput(
- value: TextFieldValue,
- imeOptions: ImeOptions,
- onEditCommand: (List<EditCommand>) -> Unit,
- onImeActionPerformed: (ImeAction) -> Unit
- ) {
- val node = textInputModifierNode ?: return
- node.launchTextInputSession {
- val input =
- CurrentInput(value, onEditCommand, onImeActionPerformed, imeOptions.imeAction)
- currentInput = input
- try {
- startInputMethod(LegacyTextInputMethodRequest(input, node.layoutCoordinates))
- } finally {
- currentInput = null
- }
- }
- }
-
- override fun stopInput() {
- job?.cancel()
- job = null
- }
-
- override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
- currentInput?.let { input -> input.value = newValue }
- }
-
- override fun notifyFocusedRect(rect: Rect) {
- currentInput?.let { input -> input.focusedRect = rect }
- }
-
- override fun startStylusHandwriting() {
- // Noop for desktop
- }
-}
-
-internal class LegacyTextInputMethodRequest(
- private val input: CurrentInput,
- private val coordinates: LayoutCoordinates?,
-) : PlatformTextInputMethodRequest, InputMethodRequests, InputMethodListener {
-
- // This is required to support input of accented characters using press-and-hold method
- // (http://support.apple.com/kb/PH11264). JDK currently properly supports this functionality
- // only for TextComponent/JTextComponent descendants. For our editor component we need this
- // workaround. After https://bugs.openjdk.java.net/browse/JDK-8074882 is fixed, this workaround
- // should be replaced with a proper solution.
- internal var charKeyPressed: Boolean = false
- private var needToDeletePreviousChar: Boolean = false
-
- override val inputMethodListener: InputMethodListener
- get() = this
-
- override val inputMethodRequests: InputMethodRequests
- get() = this
-
- override fun inputMethodTextChanged(event: InputMethodEvent) {
- if (event.isConsumed) return
- replaceInputMethodText(event)
- event.consume()
- }
-
- override fun caretPositionChanged(event: InputMethodEvent) {
- if (event.isConsumed) return
- inputMethodCaretPositionChanged(event)
- event.consume()
- }
-
- private fun inputMethodCaretPositionChanged(
- @Suppress("UNUSED_PARAMETER") event: InputMethodEvent
- ) {
- // Which OSes and which input method could produce such events? We need to have some
- // specific cases in mind before implementing this
- }
-
- // Visible for testing.
- internal fun replaceInputMethodText(event: InputMethodEvent) {
- if (event.text == null) {
- return
- }
- val committed = event.text.toStringUntil(event.committedCharacterCount)
- val composing = event.text.toStringFrom(event.committedCharacterCount)
- val ops = mutableListOf<EditCommand>()
-
- if (needToDeletePreviousChar && isMac && input.value.selection.min > 0) {
- needToDeletePreviousChar = false
- ops.add(DeleteSurroundingTextInCodePointsCommand(1, 0))
- }
-
- // newCursorPosition == 1 leads to effectively ignoring of this parameter in EditCommands
- // processing. the cursor will be set after the inserted text.
- if (committed.isNotEmpty()) {
- ops.add(CommitTextCommand(committed, 1))
- }
- if (composing.isNotEmpty()) {
- ops.add(SetComposingTextCommand(composing, 1))
- }
-
- input.onEditCommand.invoke(ops)
- }
-
- override fun getLocationOffset(x: Int, y: Int): TextHitInfo? {
- if (input.value.composition != null) {
- // TODO: to properly implement this method we need to somehow have access to
- // Paragraph at this point
- return TextHitInfo.leading(0)
- }
- return null
- }
-
- override fun cancelLatestCommittedText(
- attributes: Array<AttributedCharacterIterator.Attribute>?
- ): AttributedCharacterIterator? {
- return null
- }
-
- override fun getInsertPositionOffset(): Int {
- val composedStartIndex = input.value.composition?.start ?: 0
- val composedEndIndex = input.value.composition?.end ?: 0
-
- val caretIndex = input.value.selection.start
- if (caretIndex < composedStartIndex) {
- return caretIndex
- }
- if (caretIndex < composedEndIndex) {
- return composedStartIndex
- }
- return caretIndex - (composedEndIndex - composedStartIndex)
- }
-
- override fun getCommittedTextLength() =
- input.value.text.length - (input.value.composition?.length ?: 0)
-
- override fun getSelectedText(
- attributes: Array<AttributedCharacterIterator.Attribute>?
- ): AttributedCharacterIterator {
- if (charKeyPressed) {
- needToDeletePreviousChar = true
- }
- val str = input.value.text.substring(input.value.selection)
- return AttributedString(str).iterator
- }
-
- override fun getTextLocation(offset: TextHitInfo): Rectangle? {
- return input.focusedRect?.let {
- val (x, y) = coordinates?.localToScreen(it.topRight) ?: return null
- Rectangle(x.toInt(), y.toInt(), it.width.toInt(), it.height.toInt())
- }
- }
-
- override fun getCommittedText(
- beginIndex: Int,
- endIndex: Int,
- attributes: Array<AttributedCharacterIterator.Attribute>?
- ): AttributedCharacterIterator {
- val comp = input.value.composition
- val text = input.value.text
- val range = TextRange(beginIndex, endIndex.coerceAtMost(text.length))
- if (comp == null) {
- val res = text.substring(range)
- return AttributedString(res).iterator
- }
- val committed =
- text.substring(
- TextRange(
- min(range.min, comp.min),
- max(range.max, comp.max).coerceAtMost(text.length)
- )
- )
- return AttributedString(committed).iterator
- }
-}
-
-private fun AttributedCharacterIterator.toStringUntil(index: Int): String {
- val strBuf = StringBuffer()
- var i = index
- if (i > 0) {
- var c: Char = setIndex(0)
- while (i > 0) {
- strBuf.append(c)
- c = next()
- i--
- }
- }
- return String(strBuf)
-}
-
-private fun AttributedCharacterIterator.toStringFrom(index: Int): String {
- val strBuf = StringBuffer()
- var c: Char = setIndex(index)
- while (c != CharacterIterator.DONE) {
- strBuf.append(c)
- c = next()
- }
- return String(strBuf)
-}
-
-internal val isMac = System.getProperty("os.name").lowercase(Locale.ENGLISH).startsWith("mac")
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCache.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCache.desktop.kt
deleted file mode 100644
index 829e7d5..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCache.desktop.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text.input.internal
-
-import androidx.compose.ui.text.intl.PlatformLocale
-import androidx.compose.ui.text.style.TextDirection
-import java.text.DecimalFormatSymbols
-
-internal actual fun resolveTextDirectionForKeyboardTypePhone(
- locale: PlatformLocale
-): TextDirection {
- val symbols = DecimalFormatSymbols.getInstance(locale)
- val zero = symbols.zeroDigit
- val digitDirection = Character.getDirectionality(zero)
- return if (
- digitDirection == Character.DIRECTIONALITY_RIGHT_TO_LEFT ||
- digitDirection == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC
- ) {
- TextDirection.Rtl
- } else {
- TextDirection.Ltr
- }
-}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.desktop.kt
deleted file mode 100644
index 3ffc862..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.desktop.kt
+++ /dev/null
@@ -1,51 +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.compose.foundation.text.input.internal
-
-import androidx.compose.foundation.text.input.TextFieldCharSequence
-
-internal actual fun CharSequence.toCharArray(
- destination: CharArray,
- destinationOffset: Int,
- startIndex: Int,
- endIndex: Int
-) {
- @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
- when (this) {
- is TextFieldCharSequence ->
- toCharArray(destination, destinationOffset, startIndex, endIndex)
- is java.lang.String -> getChars(startIndex, endIndex, destination, destinationOffset)
- is StringBuilder -> getChars(startIndex, endIndex, destination, destinationOffset)
- else -> {
- require(startIndex in indices && endIndex in 0..length) {
- "Expected source [$startIndex, $endIndex) to be in [0, $length)"
- }
- val copyLength = endIndex - startIndex
- require(
- destinationOffset in destination.indices &&
- destinationOffset + copyLength in 0..destination.size
- ) {
- "Expected destination [$destinationOffset, ${destinationOffset + copyLength}) " +
- "to be in [0, ${destination.size})"
- }
-
- for (i in 0 until copyLength) {
- destination[destinationOffset + i] = get(startIndex + i)
- }
- }
- }
-}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.desktop.kt
deleted file mode 100644
index f4b0c74..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.desktop.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text.selection
-
-import androidx.compose.foundation.DesktopPlatform
-import androidx.compose.foundation.text.MappedKeys
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.isCtrlPressed
-import androidx.compose.ui.input.key.isMetaPressed
-import androidx.compose.ui.input.key.key
-
-// this doesn't sounds very sustainable
-// it would end up being a function for any conceptual keyevent (selectall, cut, copy, paste)
-// TODO(b/1564937)
-internal actual fun isCopyKeyEvent(keyEvent: KeyEvent) =
- keyEvent.key == MappedKeys.C &&
- when (DesktopPlatform.Current) {
- DesktopPlatform.MacOS -> keyEvent.isMetaPressed
- else -> keyEvent.isCtrlPressed
- } || keyEvent.key == MappedKeys.Copy
-
-/** Magnification is not supported on desktop. */
-internal actual fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier = this
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/window/WindowDraggableArea.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/window/WindowDraggableArea.desktop.kt
deleted file mode 100644
index da67d5e..0000000
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/window/WindowDraggableArea.desktop.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.window
-
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.window.WindowScope
-import java.awt.MouseInfo
-import java.awt.Point
-import java.awt.Window
-import java.awt.event.MouseAdapter
-import java.awt.event.MouseEvent
-import java.awt.event.MouseMotionAdapter
-
-/**
- * WindowDraggableArea is a component that allows you to drag the window using the mouse.
- *
- * @param modifier The modifier to be applied to the layout.
- * @param content The content lambda.
- */
-@Composable
-fun WindowScope.WindowDraggableArea(
- modifier: Modifier = Modifier,
- content: @Composable () -> Unit = {}
-) {
- val handler = remember { DragHandler(window) }
-
- Box(
- modifier =
- modifier.pointerInput(Unit) {
- awaitEachGesture {
- awaitFirstDown()
- handler.register()
- }
- }
- ) {
- content()
- }
-}
-
-private class DragHandler(private val window: Window) {
- private var location = window.location.toComposeOffset()
- private var pointStart = MouseInfo.getPointerInfo().location.toComposeOffset()
-
- private val dragListener =
- object : MouseMotionAdapter() {
- override fun mouseDragged(event: MouseEvent) = drag()
- }
- private val removeListener =
- object : MouseAdapter() {
- override fun mouseReleased(event: MouseEvent) {
- window.removeMouseMotionListener(dragListener)
- window.removeMouseListener(this)
- }
- }
-
- fun register() {
- location = window.location.toComposeOffset()
- pointStart = MouseInfo.getPointerInfo().location.toComposeOffset()
- window.addMouseListener(removeListener)
- window.addMouseMotionListener(dragListener)
- }
-
- private fun drag() {
- val point = MouseInfo.getPointerInfo().location.toComposeOffset()
- val location = location + (point - pointStart)
- window.setLocation(location.x, location.y)
- }
-
- private fun Point.toComposeOffset() = IntOffset(x, y)
-}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionHandles.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/BasicTooltip.jvmStubs.kt
similarity index 61%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionHandles.desktop.kt
copy to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/BasicTooltip.jvmStubs.kt
index c5a99e2..66b1f41 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionHandles.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/BasicTooltip.jvmStubs.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -14,21 +14,19 @@
* limitations under the License.
*/
-package androidx.compose.foundation.text.selection
+package androidx.compose.foundation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.style.ResolvedTextDirection
-import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.window.PopupPositionProvider
@Composable
-internal actual fun SelectionHandle(
- offsetProvider: OffsetProvider,
- isStartHandle: Boolean,
- direction: ResolvedTextDirection,
- handlesCrossed: Boolean,
- minTouchTargetSize: DpSize,
+actual fun BasicTooltipBox(
+ positionProvider: PopupPositionProvider,
+ tooltip: @Composable () -> Unit,
+ state: BasicTooltipState,
modifier: Modifier,
-) {
- // TODO
-}
+ focusable: Boolean,
+ enableUserInput: Boolean,
+ content: @Composable () -> Unit
+): Unit = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/Clickable.jvmStubs.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/Clickable.jvmStubs.kt
new file mode 100644
index 0000000..552054f
--- /dev/null
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/Clickable.jvmStubs.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation
+
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.node.DelegatableNode
+
+internal actual fun DelegatableNode.isComposeRootInScrollableContainer(): Boolean =
+ implementedInJetBrainsFork()
+
+internal actual val TapIndicationDelay: Long = implementedInJetBrainsFork()
+
+internal actual val KeyEvent.isPress: Boolean
+ get() = implementedInJetBrainsFork()
+
+internal actual val KeyEvent.isClick: Boolean
+ get() = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/TimeFormat.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/DarkTheme.jvmStubs.kt
similarity index 76%
copy from compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/TimeFormat.desktop.kt
copy to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/DarkTheme.jvmStubs.kt
index ca9055a..bb83308 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/TimeFormat.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/DarkTheme.jvmStubs.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package androidx.compose.material3
+package androidx.compose.foundation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
-internal actual val is24HourFormat: Boolean
- @Composable @ReadOnlyComposable get() = false
+@Composable
+@ReadOnlyComposable
+internal actual fun _isSystemInDarkTheme(): Boolean = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/DesktopOverscroll.jvmStubs.kt
similarity index 86%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/DesktopOverscroll.jvmStubs.kt
index 94ca5b1..1f77b7d 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/DesktopOverscroll.jvmStubs.kt
@@ -18,4 +18,5 @@
import androidx.compose.runtime.Composable
-@Composable internal actual fun rememberOverscrollEffect(): OverscrollEffect = NoOpOverscrollEffect
+@Composable
+internal actual fun rememberOverscrollEffect(): OverscrollEffect = implementedInJetBrainsFork()
diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/NotImplemented.jvmStubs.kt
similarity index 63%
copy from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt
copy to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/NotImplemented.jvmStubs.kt
index 195fdbc..e6fddbc 100644
--- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/NotImplemented.jvmStubs.kt
@@ -14,15 +14,14 @@
* limitations under the License.
*/
-package androidx.compose.ui.text
+package androidx.compose.foundation
-/**
- * TBD: not yet implemented.
- *
- * Converts a string with HTML tags into [AnnotatedString].
- */
-actual fun AnnotatedString.Companion.fromHtml(
- htmlString: String,
- linkStyles: TextLinkStyles?,
- linkInteractionListener: LinkInteractionListener?
-): AnnotatedString = AnnotatedString(htmlString)
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun implementedInJetBrainsFork(): Nothing =
+ throw NotImplementedError(
+ """
+ Implemented only in JetBrains fork.
+ Please use `org.jetbrains.compose.foundation:foundation` package instead.
+ """
+ .trimIndent()
+ )
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/content/MediaType.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/content/MediaType.jvmStubs.kt
similarity index 100%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/content/MediaType.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/content/MediaType.jvmStubs.kt
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/content/TransferableContent.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/content/TransferableContent.jvmStubs.kt
similarity index 80%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/content/TransferableContent.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/content/TransferableContent.jvmStubs.kt
index 4ace256..0f012d2 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/content/TransferableContent.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/content/TransferableContent.jvmStubs.kt
@@ -17,18 +17,15 @@
package androidx.compose.foundation.content
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.platform.ClipEntry
-import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.Transferable
@ExperimentalFoundationApi
actual class PlatformTransferableContent internal constructor(val transferable: Transferable)
@ExperimentalFoundationApi
-actual fun TransferableContent.hasMediaType(mediaType: MediaType): Boolean {
- return clipMetadata.isDataFlavorSupported(mediaType.dataFlavor)
-}
+actual fun TransferableContent.hasMediaType(mediaType: MediaType): Boolean =
+ implementedInJetBrainsFork()
-internal actual fun ClipEntry.readPlainText(): String? {
- return getTransferData(DataFlavor.stringFlavor) as String?
-}
+internal actual fun ClipEntry.readPlainText(): String? = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/content/internal/DragAndDropRequestPermission.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/content/internal/DragAndDropRequestPermission.jvmStubs.kt
similarity index 86%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/content/internal/DragAndDropRequestPermission.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/content/internal/DragAndDropRequestPermission.jvmStubs.kt
index 3eb9489..72de510 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/content/internal/DragAndDropRequestPermission.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/content/internal/DragAndDropRequestPermission.jvmStubs.kt
@@ -16,9 +16,9 @@
package androidx.compose.foundation.content.internal
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.draganddrop.DragAndDropEvent
import androidx.compose.ui.node.DelegatableNode
-internal actual fun DelegatableNode.dragAndDropRequestPermission(event: DragAndDropEvent) {
- /* no-op */
-}
+internal actual fun DelegatableNode.dragAndDropRequestPermission(event: DragAndDropEvent): Unit =
+ implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.jvmStubs.kt
similarity index 88%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.jvmStubs.kt
index 8cdfb48..9667098 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.jvmStubs.kt
@@ -16,12 +16,11 @@
package androidx.compose.foundation.content.internal
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.draganddrop.DragAndDropEvent
import androidx.compose.ui.draganddrop.DragAndDropModifierNode
internal actual fun ReceiveContentDragAndDropNode(
receiveContentConfiguration: ReceiveContentConfiguration,
dragAndDropRequestPermission: (DragAndDropEvent) -> Unit
-): DragAndDropModifierNode {
- return DragAndDropModifierNode()
-}
+): DragAndDropModifierNode = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.jvmStubs.kt
similarity index 67%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.jvmStubs.kt
index 4b06618..7fe1719 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.jvmStubs.kt
@@ -16,16 +16,9 @@
package androidx.compose.foundation.gestures
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.runtime.ProvidableCompositionLocal
-import androidx.compose.runtime.staticCompositionLocalOf
-/*
- * A composition local to customize the focus scrolling behavior used by some scrollable containers.
- * [LocalBringIntoViewSpec] has a platform defined behavior. The scroll default behavior will move
- * the least to bring the requested region into view.
- */
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
actual val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec> =
- staticCompositionLocalOf {
- BringIntoViewSpec.DefaultBringIntoViewSpec
- }
+ implementedInJetBrainsFork()
diff --git a/compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/gestures/DesktopScrollable.jvmStubs.kt
similarity index 65%
copy from compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.desktop.kt
copy to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/gestures/DesktopScrollable.jvmStubs.kt
index f1fe969..cb480be 100644
--- a/compose/animation/animation/src/desktopMain/kotlin/androidx/compose/animation/DesktopActualDefaultDecayAnimationSpec.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/gestures/DesktopScrollable.jvmStubs.kt
@@ -14,13 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.animation
+package androidx.compose.foundation.gestures
-import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.runtime.Composable
+import androidx.compose.foundation.implementedInJetBrainsFork
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
-@Composable
-@Deprecated("Replace with rememberSplineBasedDecay<Float>")
-actual fun defaultDecayAnimationSpec(): DecayAnimationSpec<Float> {
- return rememberSplineBasedDecay()
-}
+internal actual fun CompositionLocalConsumerModifierNode.platformScrollConfig(): ScrollConfig =
+ implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/Lazy.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/lazy/layout/Lazy.jvmStubs.kt
similarity index 81%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/Lazy.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/lazy/layout/Lazy.jvmStubs.kt
index 2f05c01..d0d2c73 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/Lazy.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/lazy/layout/Lazy.jvmStubs.kt
@@ -16,6 +16,6 @@
package androidx.compose.foundation.lazy.layout
-actual fun getDefaultLazyLayoutKey(index: Int): Any = DefaultLazyKey(index)
+import androidx.compose.foundation.implementedInJetBrainsFork
-private data class DefaultLazyKey(private val index: Int)
+actual fun getDefaultLazyLayoutKey(index: Int): Any = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.jvmStubs.kt
similarity index 80%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.jvmStubs.kt
index da15b5f..e6c77c1 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.jvmStubs.kt
@@ -17,15 +17,9 @@
package androidx.compose.foundation.lazy.layout
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.runtime.Composable
@ExperimentalFoundationApi
@Composable
-actual fun rememberDefaultPrefetchScheduler(): PrefetchScheduler {
- return NoOpPrefetchScheduler
-}
-
-@ExperimentalFoundationApi
-private object NoOpPrefetchScheduler : PrefetchScheduler {
- override fun schedulePrefetch(prefetchRequest: PrefetchRequest) {}
-}
+actual fun rememberDefaultPrefetchScheduler(): PrefetchScheduler = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.jvmStubs.kt
similarity index 65%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.jvmStubs.kt
index b9165f3..8d66a9f 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.jvmStubs.kt
@@ -16,20 +16,9 @@
package androidx.compose.foundation.relocation
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.node.DelegatableNode
/** Platform specific internal API to bring a rectangle into view. */
internal actual fun DelegatableNode.defaultBringIntoViewParent(): BringIntoViewParent =
- NoopBringIntoViewParent
-
-private object NoopBringIntoViewParent : BringIntoViewParent {
- override suspend fun bringChildIntoView(
- childCoordinates: LayoutCoordinates,
- boundsProvider: () -> Rect?
- ) {
- // TODO(b/203204124): Implement this if desktop has a
- // concept similar to Android's View.requestRectangleOnScreen.
- }
-}
+ implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/ContextMenu.jvmStubs.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/ContextMenu.jvmStubs.kt
new file mode 100644
index 0000000..9eb04eb
--- /dev/null
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/ContextMenu.jvmStubs.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.implementedInJetBrainsFork
+import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState
+import androidx.compose.foundation.text.selection.SelectionManager
+import androidx.compose.foundation.text.selection.TextFieldSelectionManager
+import androidx.compose.runtime.Composable
+
+@Composable
+internal actual fun ContextMenuArea(
+ manager: TextFieldSelectionManager,
+ content: @Composable () -> Unit
+): Unit = implementedInJetBrainsFork()
+
+// todo implement
+@Composable
+internal actual inline fun ContextMenuArea(
+ selectionState: TextFieldSelectionState,
+ enabled: Boolean,
+ content: @Composable () -> Unit
+): Unit = implementedInJetBrainsFork()
+
+@Composable
+internal actual fun ContextMenuArea(
+ manager: SelectionManager,
+ content: @Composable () -> Unit
+): Unit = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DeadKeyCombiner.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/DeadKeyCombiner.jvmStubs.kt
similarity index 82%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DeadKeyCombiner.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/DeadKeyCombiner.jvmStubs.kt
index 800f0da..90fe729 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DeadKeyCombiner.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/DeadKeyCombiner.jvmStubs.kt
@@ -16,10 +16,9 @@
package androidx.compose.foundation.text
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.utf16CodePoint
internal actual class DeadKeyCombiner {
- // TODO needs actual impl
- actual fun consume(event: KeyEvent): Int? = event.utf16CodePoint
+ actual fun consume(event: KeyEvent): Int? = implementedInJetBrainsFork()
}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCursorHandle.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/DesktopCursorHandle.jvmStubs.kt
similarity index 100%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCursorHandle.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/DesktopCursorHandle.jvmStubs.kt
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.jvmStubs.kt
similarity index 74%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.desktop.kt
copy to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.jvmStubs.kt
index a0b53a0..726724e 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.jvmStubs.kt
@@ -16,9 +16,9 @@
package androidx.compose.foundation.text
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.input.key.KeyEvent
-import org.jetbrains.skiko.orderEmojiAndSymbolsPopup
-internal actual fun KeyEvent.cancelsTextSelection(): Boolean = false
+internal actual fun KeyEvent.cancelsTextSelection(): Boolean = implementedInJetBrainsFork()
-internal actual fun showCharacterPalette() = orderEmojiAndSymbolsPopup()
+internal actual fun showCharacterPalette(): Unit = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/KeyMapping.jvmStubs.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/KeyMapping.jvmStubs.kt
new file mode 100644
index 0000000..e6450f8
--- /dev/null
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/KeyMapping.jvmStubs.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.implementedInJetBrainsFork
+import androidx.compose.ui.input.key.Key
+
+internal actual val platformDefaultKeyMapping: KeyMapping = implementedInJetBrainsFork()
+
+internal actual object MappedKeys {
+ actual val A: Key = implementedInJetBrainsFork()
+ actual val C: Key = implementedInJetBrainsFork()
+ actual val H: Key = implementedInJetBrainsFork()
+ actual val V: Key = implementedInJetBrainsFork()
+ actual val X: Key = implementedInJetBrainsFork()
+ actual val Y: Key = implementedInJetBrainsFork()
+ actual val Z: Key = implementedInJetBrainsFork()
+ actual val Backslash: Key = implementedInJetBrainsFork()
+ actual val DirectionLeft: Key = implementedInJetBrainsFork()
+ actual val DirectionRight: Key = implementedInJetBrainsFork()
+ actual val DirectionUp: Key = implementedInJetBrainsFork()
+ actual val DirectionDown: Key = implementedInJetBrainsFork()
+ actual val PageUp: Key = implementedInJetBrainsFork()
+ actual val PageDown: Key = implementedInJetBrainsFork()
+ actual val MoveHome: Key = implementedInJetBrainsFork()
+ actual val MoveEnd: Key = implementedInJetBrainsFork()
+ actual val Insert: Key = implementedInJetBrainsFork()
+ actual val Enter: Key = implementedInJetBrainsFork()
+ actual val Backspace: Key = implementedInJetBrainsFork()
+ actual val Delete: Key = implementedInJetBrainsFork()
+ actual val Paste: Key = implementedInJetBrainsFork()
+ actual val Cut: Key = implementedInJetBrainsFork()
+ actual val Copy: Key = implementedInJetBrainsFork()
+ actual val Tab: Key = implementedInJetBrainsFork()
+}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/StringHelpers.jvmStubs.kt
similarity index 72%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.desktop.kt
copy to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/StringHelpers.jvmStubs.kt
index a0b53a0..18b1f9a 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/StringHelpers.jvmStubs.kt
@@ -16,9 +16,8 @@
package androidx.compose.foundation.text
-import androidx.compose.ui.input.key.KeyEvent
-import org.jetbrains.skiko.orderEmojiAndSymbolsPopup
+import androidx.compose.foundation.implementedInJetBrainsFork
-internal actual fun KeyEvent.cancelsTextSelection(): Boolean = false
+internal actual fun String.findPrecedingBreak(index: Int): Int = implementedInJetBrainsFork()
-internal actual fun showCharacterPalette() = orderEmojiAndSymbolsPopup()
+internal actual fun String.findFollowingBreak(index: Int): Int = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DeadKeyCombiner.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TextFieldFocusModifier.jvmStubs.kt
similarity index 67%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DeadKeyCombiner.desktop.kt
copy to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TextFieldFocusModifier.jvmStubs.kt
index 800f0da..8803750 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DeadKeyCombiner.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TextFieldFocusModifier.jvmStubs.kt
@@ -16,10 +16,11 @@
package androidx.compose.foundation.text
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.utf16CodePoint
+import androidx.compose.foundation.implementedInJetBrainsFork
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusManager
-internal actual class DeadKeyCombiner {
- // TODO needs actual impl
- actual fun consume(event: KeyEvent): Int? = event.utf16CodePoint
-}
+internal actual fun Modifier.interceptDPadAndMoveFocus(
+ state: LegacyTextFieldState,
+ focusManager: FocusManager
+): Modifier = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.jvmStubs.kt
similarity index 77%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.jvmStubs.kt
index a0b53a0..9702293 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyEventHelpers.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.jvmStubs.kt
@@ -16,9 +16,8 @@
package androidx.compose.foundation.text
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.input.key.KeyEvent
-import org.jetbrains.skiko.orderEmojiAndSymbolsPopup
-internal actual fun KeyEvent.cancelsTextSelection(): Boolean = false
-
-internal actual fun showCharacterPalette() = orderEmojiAndSymbolsPopup()
+actual val KeyEvent.isTypedEvent: Boolean
+ get() = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextPointerIcon.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TextPointerIcon.jvmStubs.kt
similarity index 82%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextPointerIcon.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TextPointerIcon.jvmStubs.kt
index 7e76272..30f4d68 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextPointerIcon.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TextPointerIcon.jvmStubs.kt
@@ -16,7 +16,7 @@
package androidx.compose.foundation.text
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.input.pointer.PointerIcon
-import java.awt.Cursor
-internal actual val textPointerIcon: PointerIcon = PointerIcon(Cursor(Cursor.TEXT_CURSOR))
+internal actual val textPointerIcon: PointerIcon = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TouchMode.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TouchMode.jvmStubs.kt
similarity index 82%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TouchMode.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TouchMode.jvmStubs.kt
index 091b942..8f9ccec 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TouchMode.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/TouchMode.jvmStubs.kt
@@ -16,4 +16,6 @@
package androidx.compose.foundation.text
-internal actual val isInTouchMode = false
+import androidx.compose.foundation.implementedInJetBrainsFork
+
+internal actual val isInTouchMode: Boolean = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.jvmStubs.kt
similarity index 80%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.jvmStubs.kt
index 5667100..e213288 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.jvmStubs.kt
@@ -16,4 +16,6 @@
package androidx.compose.foundation.text.handwriting
-internal actual val isStylusHandwritingSupported: Boolean = false
+import androidx.compose.foundation.implementedInJetBrainsFork
+
+internal actual val isStylusHandwritingSupported: Boolean = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/DesktopTextInputSession.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/DesktopTextInputSession.jvmStubs.kt
similarity index 88%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/DesktopTextInputSession.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/DesktopTextInputSession.jvmStubs.kt
index c72411d2..7ed3359 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/DesktopTextInputSession.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/DesktopTextInputSession.jvmStubs.kt
@@ -17,14 +17,13 @@
package androidx.compose.foundation.text.input.internal
import androidx.compose.foundation.content.internal.ReceiveContentConfiguration
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.platform.PlatformTextInputSession
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
-import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableSharedFlow
-/** Runs desktop-specific text input session logic. */
internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSession(
state: TransformedTextFieldState,
layoutState: TextLayoutState,
@@ -33,7 +32,4 @@
onImeAction: ((ImeAction) -> Unit)?,
stylusHandwritingTrigger: MutableSharedFlow<Unit>?,
viewConfiguration: ViewConfiguration?
-): Nothing {
- // TODO(b/267235947) Wire up desktop.
- awaitCancellation()
-}
+): Nothing = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.jvmStubs.kt
similarity index 69%
copy from compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.desktop.kt
copy to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.jvmStubs.kt
index 7bf9d47..1c04d1b 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.jvmStubs.kt
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package androidx.compose.material3.internal
+package androidx.compose.foundation.text.input.internal
-import androidx.compose.ui.text.PlatformTextStyle
+import androidx.compose.foundation.implementedInJetBrainsFork
-internal actual fun defaultPlatformTextStyle(): PlatformTextStyle? = null
+internal actual fun createLegacyPlatformTextInputServiceAdapter():
+ LegacyPlatformTextInputServiceAdapter = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.jvmStubs.kt
similarity index 90%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.jvmStubs.kt
index fdccc7e..73d4929 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.jvmStubs.kt
@@ -17,13 +17,13 @@
package androidx.compose.foundation.text.input.internal
import androidx.compose.foundation.content.MediaType
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.draganddrop.DragAndDropEvent
import androidx.compose.ui.draganddrop.DragAndDropModifierNode
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.ClipMetadata
-/** System DragAndDrop is not yet supported on Desktop flavor of BTF2. */
internal actual fun textFieldDragAndDropNode(
hintMediaTypes: () -> Set<MediaType>,
onDrop: (clipEntry: ClipEntry, clipMetadata: ClipMetadata) -> Boolean,
@@ -34,6 +34,4 @@
onChanged: ((event: DragAndDropEvent) -> Unit)?,
onExited: ((event: DragAndDropEvent) -> Unit)?,
onEnded: ((event: DragAndDropEvent) -> Unit)?,
-): DragAndDropModifierNode {
- return DragAndDropModifierNode()
-}
+): DragAndDropModifierNode = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.jvmStubs.kt
similarity index 77%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.jvmStubs.kt
index 00e60c9..b127947 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.jvmStubs.kt
@@ -16,10 +16,11 @@
package androidx.compose.foundation.text.input.internal
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.input.key.KeyEvent
-/** Factory function to create a platform specific [TextFieldKeyEventHandler]. */
-internal actual fun createTextFieldKeyEventHandler() = object : TextFieldKeyEventHandler() {}
+internal actual fun createTextFieldKeyEventHandler(): TextFieldKeyEventHandler =
+ implementedInJetBrainsFork()
internal actual val KeyEvent.isFromSoftKeyboard: Boolean
- get() = false
+ get() = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCache.jvmStubs.kt
similarity index 68%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.desktop.kt
copy to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCache.jvmStubs.kt
index 00e60c9..7284a63 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldKeyEventHandler.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCache.jvmStubs.kt
@@ -16,10 +16,10 @@
package androidx.compose.foundation.text.input.internal
-import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.foundation.implementedInJetBrainsFork
+import androidx.compose.ui.text.intl.PlatformLocale
+import androidx.compose.ui.text.style.TextDirection
-/** Factory function to create a platform specific [TextFieldKeyEventHandler]. */
-internal actual fun createTextFieldKeyEventHandler() = object : TextFieldKeyEventHandler() {}
-
-internal actual val KeyEvent.isFromSoftKeyboard: Boolean
- get() = false
+internal actual fun resolveTextDirectionForKeyboardTypePhone(
+ locale: PlatformLocale
+): TextDirection = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.jvmStubs.kt
similarity index 66%
copy from compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.desktop.kt
copy to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.jvmStubs.kt
index 7bf9d47..d118d6b 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.jvmStubs.kt
@@ -14,8 +14,13 @@
* limitations under the License.
*/
-package androidx.compose.material3.internal
+package androidx.compose.foundation.text.input.internal
-import androidx.compose.ui.text.PlatformTextStyle
+import androidx.compose.foundation.implementedInJetBrainsFork
-internal actual fun defaultPlatformTextStyle(): PlatformTextStyle? = null
+internal actual fun CharSequence.toCharArray(
+ destination: CharArray,
+ destinationOffset: Int,
+ startIndex: Int,
+ endIndex: Int
+): Unit = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/selection/DesktopTextFieldMagnifier.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/selection/DesktopTextFieldMagnifier.jvmStubs.kt
similarity index 72%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/selection/DesktopTextFieldMagnifier.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/selection/DesktopTextFieldMagnifier.jvmStubs.kt
index 3a9ee1e..37623cf 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/selection/DesktopTextFieldMagnifier.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/input/internal/selection/DesktopTextFieldMagnifier.jvmStubs.kt
@@ -16,21 +16,13 @@
package androidx.compose.foundation.text.input.internal.selection
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.foundation.text.input.internal.TextLayoutState
import androidx.compose.foundation.text.input.internal.TransformedTextFieldState
-/** There is no magnifier on Desktop. Return a noop [TextFieldMagnifierNode] implementation. */
internal actual fun textFieldMagnifierNode(
textFieldState: TransformedTextFieldState,
textFieldSelectionState: TextFieldSelectionState,
textLayoutState: TextLayoutState,
visible: Boolean
-) =
- object : TextFieldMagnifierNode() {
- override fun update(
- textFieldState: TransformedTextFieldState,
- textFieldSelectionState: TextFieldSelectionState,
- textLayoutState: TextLayoutState,
- visible: Boolean
- ) {}
- }
+): TextFieldMagnifierNode = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionHandles.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionHandles.jvmStubs.kt
similarity index 90%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionHandles.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionHandles.jvmStubs.kt
index c5a99e2..ee80e2d 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionHandles.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionHandles.jvmStubs.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.text.selection
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.ResolvedTextDirection
@@ -29,6 +30,4 @@
handlesCrossed: Boolean,
minTouchTargetSize: DpSize,
modifier: Modifier,
-) {
- // TODO
-}
+): Unit = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.jvmStubs.kt
similarity index 68%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.desktop.kt
copy to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.jvmStubs.kt
index 7b9ce21..870ecf8 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.jvmStubs.kt
@@ -16,11 +16,11 @@
package androidx.compose.foundation.text.selection
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.key.KeyEvent
-internal actual val PointerEvent.isShiftPressed: Boolean
- get() = mouseEvent?.isShiftDown ?: false
+internal actual fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean = implementedInJetBrainsFork()
-/** Magnification is not supported on desktop. */
-internal actual fun Modifier.textFieldMagnifier(manager: TextFieldSelectionManager): Modifier = this
+internal actual fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier =
+ implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.desktop.kt b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.jvmStubs.kt
similarity index 83%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.desktop.kt
rename to compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.jvmStubs.kt
index 7b9ce21..0c0ad6e 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.desktop.kt
+++ b/compose/foundation/foundation/src/jvmStubsMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.jvmStubs.kt
@@ -16,11 +16,12 @@
package androidx.compose.foundation.text.selection
+import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEvent
internal actual val PointerEvent.isShiftPressed: Boolean
- get() = mouseEvent?.isShiftDown ?: false
+ get() = implementedInJetBrainsFork()
-/** Magnification is not supported on desktop. */
-internal actual fun Modifier.textFieldMagnifier(manager: TextFieldSelectionManager): Modifier = this
+internal actual fun Modifier.textFieldMagnifier(manager: TextFieldSelectionManager): Modifier =
+ implementedInJetBrainsFork()
diff --git a/compose/material3/adaptive/adaptive-layout/build.gradle b/compose/material3/adaptive/adaptive-layout/build.gradle
index 2f23b41..987896ee 100644
--- a/compose/material3/adaptive/adaptive-layout/build.gradle
+++ b/compose/material3/adaptive/adaptive-layout/build.gradle
@@ -33,14 +33,14 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
commonMain {
dependencies {
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
api(project(":compose:material3:adaptive:adaptive"))
api("androidx.compose.animation:animation-core:1.7.0-beta04")
api("androidx.compose.ui:ui:1.7.0-beta04")
@@ -61,7 +61,6 @@
jvmMain {
dependsOn(commonMain)
dependencies {
- implementation(libs.kotlinStdlib)
}
}
@@ -73,7 +72,7 @@
}
}
- desktopMain {
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt b/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/NotImplemented.jvmStubs.kt
similarity index 67%
copy from compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
copy to compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/NotImplemented.jvmStubs.kt
index d4a754b..a2e9a00 100644
--- a/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/NotImplemented.jvmStubs.kt
@@ -16,6 +16,12 @@
package androidx.compose.material3.adaptive.layout
-import androidx.compose.ui.Modifier
-
-internal actual fun Modifier.systemGestureExclusion(): Modifier = this
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun implementedInJetBrainsFork(): Nothing =
+ throw NotImplementedError(
+ """
+ Implemented only in JetBrains fork.
+ Please use `org.jetbrains.compose.material3.adaptive:adaptive-layout` package instead.
+ """
+ .trimIndent()
+ )
diff --git a/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt b/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.jvmStubs.kt
similarity index 95%
rename from compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
rename to compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.jvmStubs.kt
index d4a754b..f8a917f 100644
--- a/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.jvmStubs.kt
@@ -18,4 +18,4 @@
import androidx.compose.ui.Modifier
-internal actual fun Modifier.systemGestureExclusion(): Modifier = this
+internal actual fun Modifier.systemGestureExclusion(): Modifier = implementedInJetBrainsFork()
diff --git a/compose/material3/adaptive/adaptive-navigation/build.gradle b/compose/material3/adaptive/adaptive-navigation/build.gradle
index 982a98c..e6f2983 100644
--- a/compose/material3/adaptive/adaptive-navigation/build.gradle
+++ b/compose/material3/adaptive/adaptive-navigation/build.gradle
@@ -33,14 +33,14 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
commonMain {
dependencies {
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
api(project(":compose:material3:adaptive:adaptive-layout"))
implementation("androidx.compose.foundation:foundation:1.6.5")
implementation("androidx.compose.ui:ui-util:1.6.5")
@@ -55,7 +55,6 @@
jvmMain {
dependsOn(commonMain)
dependencies {
- implementation(libs.kotlinStdlib)
}
}
@@ -68,7 +67,7 @@
}
}
- desktopMain {
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
}
diff --git a/compose/material3/adaptive/adaptive/build.gradle b/compose/material3/adaptive/adaptive/build.gradle
index b478685..2e28d64 100644
--- a/compose/material3/adaptive/adaptive/build.gradle
+++ b/compose/material3/adaptive/adaptive/build.gradle
@@ -33,14 +33,14 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
commonMain {
dependencies {
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
api("androidx.compose.foundation:foundation:1.6.5")
api("androidx.compose.ui:ui-geometry:1.6.5")
api("androidx.window:window-core:1.3.0-rc01")
@@ -55,7 +55,6 @@
jvmMain {
dependsOn(commonMain)
dependencies {
- implementation(libs.kotlinStdlib)
}
}
@@ -68,7 +67,7 @@
}
}
- desktopMain {
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
}
diff --git a/compose/material3/adaptive/adaptive/src/desktopMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.desktop.kt b/compose/material3/adaptive/adaptive/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.jvmStubs.kt
similarity index 87%
rename from compose/material3/adaptive/adaptive/src/desktopMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.desktop.kt
rename to compose/material3/adaptive/adaptive/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.jvmStubs.kt
index a32d0d4..a85f34a 100644
--- a/compose/material3/adaptive/adaptive/src/desktopMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.desktop.kt
+++ b/compose/material3/adaptive/adaptive/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.jvmStubs.kt
@@ -19,6 +19,4 @@
import androidx.compose.runtime.Composable
@Composable
-actual fun currentWindowAdaptiveInfo(): WindowAdaptiveInfo {
- TODO("Not yet implemented")
-}
+actual fun currentWindowAdaptiveInfo(): WindowAdaptiveInfo = implementedInJetBrainsFork()
diff --git a/compose/material3/adaptive/adaptive/src/desktopMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.desktop.kt b/compose/material3/adaptive/adaptive/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/NotImplemented.jvmStubs.kt
similarity index 67%
copy from compose/material3/adaptive/adaptive/src/desktopMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.desktop.kt
copy to compose/material3/adaptive/adaptive/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/NotImplemented.jvmStubs.kt
index a32d0d4..53311a1 100644
--- a/compose/material3/adaptive/adaptive/src/desktopMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.desktop.kt
+++ b/compose/material3/adaptive/adaptive/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/NotImplemented.jvmStubs.kt
@@ -16,9 +16,12 @@
package androidx.compose.material3.adaptive
-import androidx.compose.runtime.Composable
-
-@Composable
-actual fun currentWindowAdaptiveInfo(): WindowAdaptiveInfo {
- TODO("Not yet implemented")
-}
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun implementedInJetBrainsFork(): Nothing =
+ throw NotImplementedError(
+ """
+ Implemented only in JetBrains fork.
+ Please use `org.jetbrains.compose.material3.adaptive:adaptive` package instead.
+ """
+ .trimIndent()
+ )
diff --git a/compose/material3/material3-adaptive-navigation-suite/build.gradle b/compose/material3/material3-adaptive-navigation-suite/build.gradle
index 3c2b933..35ca4949 100644
--- a/compose/material3/material3-adaptive-navigation-suite/build.gradle
+++ b/compose/material3/material3-adaptive-navigation-suite/build.gradle
@@ -33,14 +33,14 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
commonMain {
dependencies {
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
api(project(":compose:material3:material3"))
api(project(":compose:material3:adaptive:adaptive"))
implementation("androidx.compose.ui:ui-util:1.6.0")
@@ -56,7 +56,6 @@
jvmMain {
dependsOn(commonMain)
dependencies {
- implementation(libs.kotlinStdlib)
}
}
@@ -68,7 +67,7 @@
}
}
- desktopMain {
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
}
diff --git a/compose/material3/material3-common/build.gradle b/compose/material3/material3-common/build.gradle
index 077b116..286e981 100644
--- a/compose/material3/material3-common/build.gradle
+++ b/compose/material3/material3-common/build.gradle
@@ -34,14 +34,14 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
commonMain {
dependencies {
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
implementation(project(":compose:ui:ui-util"))
api("androidx.compose.foundation:foundation:1.6.0")
api("androidx.compose.foundation:foundation-layout:1.6.0")
@@ -50,8 +50,6 @@
api("androidx.compose.ui:ui-text:1.6.0")
}
}
- androidMain.dependencies {
- }
commonTest {
dependencies {
@@ -61,7 +59,6 @@
jvmMain {
dependsOn(commonMain)
dependencies {
- implementation(libs.kotlinStdlib)
}
}
@@ -72,7 +69,7 @@
}
}
- desktopMain {
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
}
diff --git a/compose/material3/material3-window-size-class/build.gradle b/compose/material3/material3-window-size-class/build.gradle
index 07887b4..1f443e0 100644
--- a/compose/material3/material3-window-size-class/build.gradle
+++ b/compose/material3/material3-window-size-class/build.gradle
@@ -32,14 +32,14 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
commonMain {
dependencies {
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
implementation("androidx.compose.ui:ui-util:1.6.0")
api("androidx.compose.runtime:runtime:1.7.0-beta02")
api("androidx.compose.ui:ui:1.6.0")
@@ -49,20 +49,12 @@
commonTest {
dependencies {
-
}
}
jvmMain {
dependsOn(commonMain)
dependencies {
- implementation(libs.kotlinStdlib)
- }
- }
-
- skikoMain {
- dependsOn(commonMain)
- dependencies {
}
}
@@ -74,11 +66,9 @@
}
}
- desktopMain {
- dependsOn(skikoMain)
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
-
}
}
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 4ed0b77..57ba2af 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -43,10 +43,13 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional int titleHorizontalAlignment, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional int titleHorizontalAlignment, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional int titleHorizontalAlignment, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.BottomAppBarState rememberBottomAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TopAppBarState rememberTopAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
}
@@ -1118,8 +1121,8 @@
}
public final class MaterialShapesKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.ui.graphics.Path toPath(androidx.graphics.shapes.RoundedPolygon, optional androidx.compose.ui.graphics.Path path, optional int startAngle);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.ui.graphics.Shape toShape(androidx.graphics.shapes.RoundedPolygon, optional int startAngle);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.Path toPath(androidx.graphics.shapes.RoundedPolygon, optional int startAngle);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.Shape toShape(androidx.graphics.shapes.RoundedPolygon, optional int startAngle);
}
public final class MaterialTheme {
@@ -2346,8 +2349,12 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior exitUntilCollapsedScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
method public float getLargeAppBarCollapsedHeight();
method public float getLargeAppBarExpandedHeight();
+ method public float getLargeAppBarWithSubtitleExpandedHeight();
+ method public float getLargeAppBarWithoutSubtitleExpandedHeight();
method public float getMediumAppBarCollapsedHeight();
method public float getMediumAppBarExpandedHeight();
+ method public float getMediumAppBarWithSubtitleExpandedHeight();
+ method public float getMediumAppBarWithoutSubtitleExpandedHeight();
method public float getTopAppBarExpandedHeight();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors largeTopAppBarColors();
@@ -2359,8 +2366,12 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors topAppBarColors(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconContentColor, optional long titleContentColor, optional long actionIconContentColor);
property public final float LargeAppBarCollapsedHeight;
property public final float LargeAppBarExpandedHeight;
+ property public final float LargeAppBarWithSubtitleExpandedHeight;
+ property public final float LargeAppBarWithoutSubtitleExpandedHeight;
property public final float MediumAppBarCollapsedHeight;
property public final float MediumAppBarExpandedHeight;
+ property public final float MediumAppBarWithSubtitleExpandedHeight;
+ property public final float MediumAppBarWithoutSubtitleExpandedHeight;
property public final float TopAppBarExpandedHeight;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
field public static final androidx.compose.material3.TopAppBarDefaults INSTANCE;
@@ -2402,6 +2413,19 @@
property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.TopAppBarState,? extends java.lang.Object?> Saver;
}
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class TopAppBarTitleAlignment {
+ field public static final androidx.compose.material3.TopAppBarTitleAlignment.Companion Companion;
+ }
+
+ public static final class TopAppBarTitleAlignment.Companion {
+ method public int getCenter();
+ method public int getEnd();
+ method public int getStart();
+ property public final int Center;
+ property public final int End;
+ property public final int Start;
+ }
+
@androidx.compose.runtime.Immutable public final class Typography {
ctor public Typography(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
method public androidx.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 4ed0b77..57ba2af 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -43,10 +43,13 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional int titleHorizontalAlignment, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional int titleHorizontalAlignment, optional float collapsedHeight, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional int titleHorizontalAlignment, optional float expandedHeight, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.BottomAppBarState rememberBottomAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TopAppBarState rememberTopAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
}
@@ -1118,8 +1121,8 @@
}
public final class MaterialShapesKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.ui.graphics.Path toPath(androidx.graphics.shapes.RoundedPolygon, optional androidx.compose.ui.graphics.Path path, optional int startAngle);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.ui.graphics.Shape toShape(androidx.graphics.shapes.RoundedPolygon, optional int startAngle);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.Path toPath(androidx.graphics.shapes.RoundedPolygon, optional int startAngle);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.Shape toShape(androidx.graphics.shapes.RoundedPolygon, optional int startAngle);
}
public final class MaterialTheme {
@@ -2346,8 +2349,12 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior exitUntilCollapsedScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
method public float getLargeAppBarCollapsedHeight();
method public float getLargeAppBarExpandedHeight();
+ method public float getLargeAppBarWithSubtitleExpandedHeight();
+ method public float getLargeAppBarWithoutSubtitleExpandedHeight();
method public float getMediumAppBarCollapsedHeight();
method public float getMediumAppBarExpandedHeight();
+ method public float getMediumAppBarWithSubtitleExpandedHeight();
+ method public float getMediumAppBarWithoutSubtitleExpandedHeight();
method public float getTopAppBarExpandedHeight();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors largeTopAppBarColors();
@@ -2359,8 +2366,12 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors topAppBarColors(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconContentColor, optional long titleContentColor, optional long actionIconContentColor);
property public final float LargeAppBarCollapsedHeight;
property public final float LargeAppBarExpandedHeight;
+ property public final float LargeAppBarWithSubtitleExpandedHeight;
+ property public final float LargeAppBarWithoutSubtitleExpandedHeight;
property public final float MediumAppBarCollapsedHeight;
property public final float MediumAppBarExpandedHeight;
+ property public final float MediumAppBarWithSubtitleExpandedHeight;
+ property public final float MediumAppBarWithoutSubtitleExpandedHeight;
property public final float TopAppBarExpandedHeight;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
field public static final androidx.compose.material3.TopAppBarDefaults INSTANCE;
@@ -2402,6 +2413,19 @@
property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.TopAppBarState,? extends java.lang.Object?> Saver;
}
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class TopAppBarTitleAlignment {
+ field public static final androidx.compose.material3.TopAppBarTitleAlignment.Companion Companion;
+ }
+
+ public static final class TopAppBarTitleAlignment.Companion {
+ method public int getCenter();
+ method public int getEnd();
+ method public int getStart();
+ property public final int Center;
+ property public final int End;
+ property public final int Start;
+ }
+
@androidx.compose.runtime.Immutable public final class Typography {
ctor public Typography(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
method public androidx.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 6a0e8c7..07129e1 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -32,14 +32,14 @@
androidXMultiplatform {
android()
- desktop()
+ jvmStubs()
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
commonMain {
dependencies {
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
// Keep pinned unless there is a need for tip of tree behavior
implementation("androidx.collection:collection:1.4.0")
implementation("androidx.compose.animation:animation-core:1.6.0")
@@ -66,12 +66,6 @@
}
}
- skikoMain {
- dependsOn(commonMain)
- dependencies {
- }
- }
-
androidMain {
dependsOn(jvmMain)
dependencies {
@@ -83,11 +77,9 @@
}
}
- desktopMain {
- dependsOn(skikoMain)
+ jvmStubsMain {
dependsOn(jvmMain)
dependencies {
- implementation(libs.kotlinStdlib)
}
}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 5b38076..3d68f05 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -64,6 +64,8 @@
import androidx.compose.material3.samples.ElevatedSuggestionChipSample
import androidx.compose.material3.samples.EnterAlwaysTopAppBar
import androidx.compose.material3.samples.ExitAlwaysBottomAppBar
+import androidx.compose.material3.samples.ExitUntilCollapsedCenterAlignedLargeTopAppBarWithSubtitle
+import androidx.compose.material3.samples.ExitUntilCollapsedCenterAlignedMediumTopAppBarWithSubtitle
import androidx.compose.material3.samples.ExitUntilCollapsedLargeTopAppBar
import androidx.compose.material3.samples.ExitUntilCollapsedMediumTopAppBar
import androidx.compose.material3.samples.ExposedDropdownMenuSample
@@ -157,9 +159,11 @@
import androidx.compose.material3.samples.SimpleBottomAppBar
import androidx.compose.material3.samples.SimpleBottomSheetScaffoldSample
import androidx.compose.material3.samples.SimpleCenterAlignedTopAppBar
+import androidx.compose.material3.samples.SimpleCenterAlignedTopAppBarWithSubtitle
import androidx.compose.material3.samples.SimpleOutlinedTextFieldSample
import androidx.compose.material3.samples.SimpleTextFieldSample
import androidx.compose.material3.samples.SimpleTopAppBar
+import androidx.compose.material3.samples.SimpleTopAppBarWithSubtitle
import androidx.compose.material3.samples.SliderSample
import androidx.compose.material3.samples.SliderWithCustomThumbSample
import androidx.compose.material3.samples.SliderWithCustomTrackAndThumb
@@ -610,6 +614,13 @@
SimpleTopAppBar()
},
Example(
+ name = "SimpleTopAppBarWithSubtitle",
+ description = TopAppBarExampleDescription,
+ sourceUrl = TopAppBarExampleSourceUrl,
+ ) {
+ SimpleTopAppBarWithSubtitle()
+ },
+ Example(
name = "SimpleCenterAlignedTopAppBar",
description = TopAppBarExampleDescription,
sourceUrl = TopAppBarExampleSourceUrl,
@@ -617,6 +628,13 @@
SimpleCenterAlignedTopAppBar()
},
Example(
+ name = "SimpleCenterAlignedTopAppBarWithSubtitle",
+ description = TopAppBarExampleDescription,
+ sourceUrl = TopAppBarExampleSourceUrl,
+ ) {
+ SimpleCenterAlignedTopAppBarWithSubtitle()
+ },
+ Example(
name = "PinnedTopAppBar",
description = TopAppBarExampleDescription,
sourceUrl = TopAppBarExampleSourceUrl,
@@ -638,12 +656,26 @@
ExitUntilCollapsedMediumTopAppBar()
},
Example(
+ name = "ExitUntilCollapsedCenterAlignedMediumTopAppBarWithSubtitle",
+ description = TopAppBarExampleDescription,
+ sourceUrl = TopAppBarExampleSourceUrl,
+ ) {
+ ExitUntilCollapsedCenterAlignedMediumTopAppBarWithSubtitle()
+ },
+ Example(
name = "ExitUntilCollapsedLargeTopAppBar",
description = TopAppBarExampleDescription,
sourceUrl = TopAppBarExampleSourceUrl,
) {
ExitUntilCollapsedLargeTopAppBar()
},
+ Example(
+ name = "ExitUntilCollapsedCenterAlignedLargeTopAppBarWithSubtitle",
+ description = TopAppBarExampleDescription,
+ sourceUrl = TopAppBarExampleSourceUrl,
+ ) {
+ ExitUntilCollapsedCenterAlignedLargeTopAppBarWithSubtitle()
+ },
)
private const val FloatingAppBarsExampleDescription = "Floating app bar examples"
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
index 04fbae7..5a92111 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
@@ -32,6 +32,7 @@
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
@@ -44,6 +45,7 @@
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarTitleAlignment
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -104,6 +106,59 @@
}
/**
+ * A sample for a simple use of small [TopAppBar] with a subtitle.
+ *
+ * The top app bar here does not react to any scroll events in the content under it.
+ */
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun SimpleTopAppBarWithSubtitle() {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = {
+ Text("Simple TopAppBar", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ },
+ subtitle = { Text("Subtitle", maxLines = 1, overflow = TextOverflow.Ellipsis) },
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = "Localized description"
+ )
+ }
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Localized description"
+ )
+ }
+ }
+ )
+ },
+ content = { innerPadding ->
+ LazyColumn(
+ contentPadding = innerPadding,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ val list = (0..75).map { it.toString() }
+ items(count = list.size) {
+ Text(
+ text = list[it],
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
+ )
+ }
+ }
+ }
+ )
+}
+
+/**
* A sample for a simple use of [CenterAlignedTopAppBar].
*
* The top app bar here does not react to any scroll events in the content under it.
@@ -156,6 +211,60 @@
}
/**
+ * A sample for a simple use of small [CenterAlignedTopAppBar] with a subtitle.
+ *
+ * The top app bar here does not react to any scroll events in the content under it.
+ */
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun SimpleCenterAlignedTopAppBarWithSubtitle() {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = {
+ Text("Simple TopAppBar", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ },
+ subtitle = { Text("Subtitle", maxLines = 1, overflow = TextOverflow.Ellipsis) },
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Center,
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = "Localized description"
+ )
+ }
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Localized description"
+ )
+ }
+ }
+ )
+ },
+ content = { innerPadding ->
+ LazyColumn(
+ contentPadding = innerPadding,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ val list = (0..75).map { it.toString() }
+ items(count = list.size) {
+ Text(
+ text = list[it],
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
+ )
+ }
+ }
+ }
+ )
+}
+
+/**
* A sample for a pinned small [TopAppBar].
*
* The top app bar here is pinned to its location and changes its container color when the content
@@ -220,7 +329,7 @@
* A sample for a small [TopAppBar] that collapses when the content is scrolled up, and appears when
* the content scrolled down.
*/
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Sampled
@Composable
@@ -231,6 +340,7 @@
topBar = {
TopAppBar(
title = { Text("TopAppBar", maxLines = 1, overflow = TextOverflow.Ellipsis) },
+ subtitle = { Text("Subtitle", maxLines = 1, overflow = TextOverflow.Ellipsis) },
navigationIcon = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(
@@ -323,6 +433,62 @@
}
/**
+ * A sample for a [MediumTopAppBar] that collapses when the content is scrolled up, and appears when
+ * the content is completely scrolled back down, centered with subtitle.
+ */
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun ExitUntilCollapsedCenterAlignedMediumTopAppBarWithSubtitle() {
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ topBar = {
+ MediumTopAppBar(
+ title = {
+ Text("Medium TopAppBar", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ },
+ subtitle = { Text("Subtitle", maxLines = 1, overflow = TextOverflow.Ellipsis) },
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Center,
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = "Localized description"
+ )
+ }
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Localized description"
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+ },
+ content = { innerPadding ->
+ LazyColumn(
+ contentPadding = innerPadding,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ val list = (0..75).map { it.toString() }
+ items(count = list.size) {
+ Text(
+ text = list[it],
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
+ )
+ }
+ }
+ }
+ )
+}
+
+/**
* A sample for a [LargeTopAppBar] that collapses when the content is scrolled up, and appears when
* the content is completely scrolled back down.
*/
@@ -374,6 +540,60 @@
)
}
+/**
+ * A sample for a [LargeTopAppBar] that collapses when the content is scrolled up, and appears when
+ * the content is completely scrolled back down, centered with subtitle.
+ */
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun ExitUntilCollapsedCenterAlignedLargeTopAppBarWithSubtitle() {
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ topBar = {
+ LargeTopAppBar(
+ title = { Text("Large TopAppBar", maxLines = 1, overflow = TextOverflow.Ellipsis) },
+ subtitle = { Text("Subtitle", maxLines = 1, overflow = TextOverflow.Ellipsis) },
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Center,
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = "Localized description"
+ )
+ }
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Localized description"
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+ },
+ content = { innerPadding ->
+ LazyColumn(
+ contentPadding = innerPadding,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ val list = (0..75).map { it.toString() }
+ items(count = list.size) {
+ Text(
+ text = list[it],
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
+ )
+ }
+ }
+ }
+ )
+}
+
@Preview
@Sampled
@Composable
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
index de222ab..40dce46 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
@@ -47,7 +47,7 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
class AppBarScreenshotTest {
@get:Rule val composeTestRule = createComposeRule()
@@ -81,6 +81,60 @@
}
@Test
+ fun smallAppBar_withSubtitle_lightTheme() {
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = { Text("Title") },
+ subtitle = { Text("Subtitle") },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Like")
+ }
+ }
+ )
+ }
+ }
+
+ assertAppBarAgainstGolden(goldenIdentifier = "smallAppBar_withSubtitle_lightTheme")
+ }
+
+ @Test
+ fun smallAppBar_withoutSubtitle_lightTheme() {
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = { Text("Title") },
+ subtitle = {},
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Like")
+ }
+ }
+ )
+ }
+ }
+
+ assertAppBarAgainstGolden(goldenIdentifier = "smallAppBar_withoutSubtitle_lightTheme")
+ }
+
+ @Test
fun smallAppBar_lightTheme_clipsWhenCollapsedWithInsets() {
composeTestRule.setMaterialContent(lightColorScheme()) {
val behavior = enterAlwaysScrollBehavior(rememberTopAppBarState())
@@ -170,6 +224,64 @@
}
@Test
+ fun centerAlignedAppBar_withSubtitle_lightTheme() {
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = { Text("Title") },
+ subtitle = { Text("Subtitle") },
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Center,
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Like")
+ }
+ }
+ )
+ }
+ }
+
+ assertAppBarAgainstGolden(goldenIdentifier = "centerAlignedAppBar_withSubtitle_lightTheme")
+ }
+
+ @Test
+ fun centerAlignedAppBar_withoutSubtitle_lightTheme() {
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = { Text("Title") },
+ subtitle = {},
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Center,
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Like")
+ }
+ }
+ )
+ }
+ }
+
+ assertAppBarAgainstGolden(
+ goldenIdentifier = "centerAlignedAppBar_withoutSubtitle_lightTheme"
+ )
+ }
+
+ @Test
fun centerAlignedAppBar_darkTheme() {
composeTestRule.setMaterialContent(darkColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
@@ -222,6 +334,95 @@
}
@Test
+ fun mediumAppBar_centerAligned_withSubtitle_lightTheme() {
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ MediumTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = { Text("Title") },
+ subtitle = { Text("Subtitle") },
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Center,
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Like")
+ }
+ }
+ )
+ }
+ }
+
+ assertAppBarAgainstGolden(
+ goldenIdentifier = "mediumAppBar_centerAligned_withSubtitle_lightTheme"
+ )
+ }
+
+ @Test
+ fun mediumAppBar_centerAligned_withoutSubtitle_lightTheme() {
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ MediumTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = { Text("Title") },
+ subtitle = null,
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Center,
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Like")
+ }
+ }
+ )
+ }
+ }
+
+ assertAppBarAgainstGolden(
+ goldenIdentifier = "mediumAppBar_centerAligned_withoutSubtitle_lightTheme"
+ )
+ }
+
+ @Test
+ fun mediumAppBar_startAligned_withSubtitle_darkTheme() {
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ MediumTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = { Text("Title") },
+ subtitle = { Text("Subtitle") },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Like")
+ }
+ }
+ )
+ }
+ }
+
+ assertAppBarAgainstGolden(
+ goldenIdentifier = "mediumAppBar_startAligned_withSubtitle_darkTheme"
+ )
+ }
+
+ @Test
fun mediumAppBar_darkTheme() {
composeTestRule.setMaterialContent(darkColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
@@ -274,6 +475,95 @@
}
@Test
+ fun largeAppBar_centerAligned_withSubtitle_lightTheme() {
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ LargeTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = { Text("Title") },
+ subtitle = { Text("Subtitle") },
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Center,
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Like")
+ }
+ }
+ )
+ }
+ }
+
+ assertAppBarAgainstGolden(
+ goldenIdentifier = "largeAppBar_centerAligned_withSubtitle_lightTheme"
+ )
+ }
+
+ @Test
+ fun largeAppBar_centerAligned_withoutSubtitle_lightTheme() {
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ LargeTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = { Text("Title") },
+ subtitle = null,
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Center,
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Like")
+ }
+ }
+ )
+ }
+ }
+
+ assertAppBarAgainstGolden(
+ goldenIdentifier = "largeAppBar_centerAligned_withoutSubtitle_lightTheme"
+ )
+ }
+
+ @Test
+ fun largeAppBar_startAligned_withSubtitle_darkTheme() {
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ LargeTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = { Text("Title") },
+ subtitle = { Text("Subtitle") },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Like")
+ }
+ }
+ )
+ }
+ }
+
+ assertAppBarAgainstGolden(
+ goldenIdentifier = "largeAppBar_startAligned_withSubtitle_darkTheme"
+ )
+ }
+
+ @Test
fun largeAppBar_darkTheme() {
composeTestRule.setMaterialContent(darkColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt
index 410f4d9..d3e034f 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt
@@ -37,6 +37,7 @@
import androidx.compose.material3.tokens.TopAppBarMediumTokens
import androidx.compose.material3.tokens.TopAppBarSmallCenteredTokens
import androidx.compose.material3.tokens.TopAppBarSmallTokens
+import androidx.compose.material3.tokens.TypographyKeyTokens
import androidx.compose.runtime.Composable
import androidx.compose.testutils.assertContainsColor
import androidx.compose.ui.Modifier
@@ -85,7 +86,7 @@
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@MediumTest
@RunWith(AndroidJUnit4::class)
class AppBarTest {
@@ -110,6 +111,17 @@
}
@Test
+ fun smallTopAppBar_withSubtitle() {
+ val subtitle = "Subtitle"
+ rule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ TopAppBar(title = { Text("Title") }, subtitle = { Text(subtitle) })
+ }
+ }
+ rule.onNodeWithText(subtitle).assertIsDisplayed()
+ }
+
+ @Test
fun smallTopAppBar_default_positioning() {
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
@@ -153,6 +165,24 @@
assertThat(textStyle).isEqualTo(expectedTextStyle)
}
+ @Test
+ fun smallTopAppBar_subtitleDefaultStyle() {
+ var textStyle: TextStyle? = null
+ var expectedTextStyle: TextStyle? = null
+ rule.setMaterialContent(lightColorScheme()) {
+ TopAppBar(
+ title = { Text("Title") },
+ subtitle = {
+ Text("Subtitle")
+ textStyle = LocalTextStyle.current
+ expectedTextStyle = TypographyKeyTokens.LabelMedium.value // TODO tokens
+ }
+ )
+ }
+ assertThat(textStyle).isNotNull()
+ assertThat(textStyle).isEqualTo(expectedTextStyle)
+ }
+
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun smallTopAppBar_contentColor() {
@@ -331,6 +361,17 @@
}
@Test
+ fun centerAlignedTopAppBar_withSubtitle() {
+ val subtitle = "Subtitle"
+ rule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TopAppBarTestTag)) {
+ TopAppBar(title = { Text("Title") }, subtitle = { Text(subtitle) })
+ }
+ }
+ rule.onNodeWithText(subtitle).assertIsDisplayed()
+ }
+
+ @Test
fun centerAlignedTopAppBar_default_positioning() {
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
@@ -450,6 +491,24 @@
}
@Test
+ fun centerAlignedTopAppBar_subtitleDefaultStyle() {
+ var textStyle: TextStyle? = null
+ var expectedTextStyle: TextStyle? = null
+ rule.setMaterialContent(lightColorScheme()) {
+ TopAppBar(
+ title = { Text("Title") },
+ subtitle = {
+ Text("Subtitle")
+ textStyle = LocalTextStyle.current
+ expectedTextStyle = TypographyKeyTokens.LabelMedium.value // TODO tokens
+ }
+ )
+ }
+ assertThat(textStyle).isNotNull()
+ assertThat(textStyle).isEqualTo(expectedTextStyle)
+ }
+
+ @Test
fun centerAlignedTopAppBar_measureWithNonZeroMinWidth() {
var appBarSize = IntSize.Zero
rule.setMaterialContent(lightColorScheme()) {
@@ -670,6 +729,7 @@
appBarMaxHeight = TopAppBarMediumTokens.ContainerHeight,
appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
titleContentColor = Color.Unspecified,
+ subtitleContentColor = Color.Unspecified,
content = content
)
}
@@ -691,6 +751,59 @@
appBarMaxHeight = TopAppBarMediumTokens.ContainerHeight,
appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
titleContentColor = Color.Green,
+ subtitleContentColor = Color.Unspecified,
+ content = content
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun mediumTopAppBar_scrolledColorsWithCustomTitleAndSubtitleTextColor() {
+ val content =
+ @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+ MediumTopAppBar(
+ modifier = Modifier.testTag(TopAppBarTestTag),
+ title = {
+ Text(text = "Title", Modifier.testTag(TitleTestTag), color = Color.Green)
+ },
+ subtitle = {
+ Text(
+ text = "Subtitle",
+ Modifier.testTag(SubtitleTestTag),
+ color = Color.Green
+ )
+ },
+ scrollBehavior = scrollBehavior
+ )
+ }
+ assertMediumOrLargeScrolledColors(
+ appBarMaxHeight = TopAppBarDefaults.MediumAppBarWithSubtitleExpandedHeight,
+ appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
+ titleContentColor = Color.Green,
+ subtitleContentColor = Color.Green,
+ content = content
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun mediumTopAppBar_scrolledColorsWithCustomTitleAndWithoutSubtitleTextColor() {
+ val content =
+ @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+ MediumTopAppBar(
+ modifier = Modifier.testTag(TopAppBarTestTag),
+ title = {
+ Text(text = "Title", Modifier.testTag(TitleTestTag), color = Color.Green)
+ },
+ subtitle = null,
+ scrollBehavior = scrollBehavior
+ )
+ }
+ assertMediumOrLargeScrolledColors(
+ appBarMaxHeight = TopAppBarDefaults.MediumAppBarWithoutSubtitleExpandedHeight,
+ appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
+ titleContentColor = Color.Green,
+ subtitleContentColor = Color.Unspecified,
content = content
)
}
@@ -723,7 +836,29 @@
assertMediumOrLargeScrolledSemantics(
TopAppBarMediumTokens.ContainerHeight,
TopAppBarSmallTokens.ContainerHeight,
- content
+ content,
+ withSubtitle = false
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun mediumTopAppBar_withSubtitle_semantics() {
+ val content =
+ @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+ MediumTopAppBar(
+ modifier = Modifier.testTag(TopAppBarTestTag),
+ title = { Text("Title", Modifier.testTag(TitleTestTag)) },
+ subtitle = { Text("Subtitle", Modifier.testTag(SubtitleTestTag)) },
+ scrollBehavior = scrollBehavior
+ )
+ }
+
+ assertMediumOrLargeScrolledSemantics(
+ TopAppBarMediumTokens.ContainerHeight,
+ TopAppBarSmallTokens.ContainerHeight,
+ content,
+ withSubtitle = true
)
}
@@ -741,7 +876,28 @@
assertMediumOrLargeScrolledSemantics(
TopAppBarLargeTokens.ContainerHeight,
TopAppBarSmallTokens.ContainerHeight,
- content
+ content,
+ withSubtitle = false
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun largeTopAppBar_withSubtitle_semantics() {
+ val content =
+ @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+ LargeTopAppBar(
+ modifier = Modifier.testTag(TopAppBarTestTag),
+ title = { Text("Title", Modifier.testTag(TitleTestTag)) },
+ subtitle = { Text("Subtitle", Modifier.testTag(SubtitleTestTag)) },
+ scrollBehavior = scrollBehavior
+ )
+ }
+ assertMediumOrLargeScrolledSemantics(
+ TopAppBarLargeTokens.ContainerHeight,
+ TopAppBarSmallTokens.ContainerHeight,
+ content,
+ withSubtitle = true
)
}
@@ -862,6 +1018,7 @@
appBarMaxHeight = TopAppBarLargeTokens.ContainerHeight,
appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
titleContentColor = Color.Unspecified,
+ subtitleContentColor = Color.Unspecified,
content = content
)
}
@@ -883,6 +1040,59 @@
appBarMaxHeight = TopAppBarLargeTokens.ContainerHeight,
appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
titleContentColor = Color.Red,
+ subtitleContentColor = Color.Unspecified,
+ content = content
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun largeTopAppBar_scrolledColorsWithCustomTitleAndSubtitleTextColor() {
+ val content =
+ @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+ LargeTopAppBar(
+ modifier = Modifier.testTag(TopAppBarTestTag),
+ title = {
+ Text(text = "Title", Modifier.testTag(TitleTestTag), color = Color.Red)
+ },
+ subtitle = {
+ Text(
+ text = "Subtitle",
+ Modifier.testTag(SubtitleTestTag),
+ color = Color.Red
+ )
+ },
+ scrollBehavior = scrollBehavior,
+ )
+ }
+ assertMediumOrLargeScrolledColors(
+ appBarMaxHeight = TopAppBarDefaults.LargeAppBarWithSubtitleExpandedHeight,
+ appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
+ titleContentColor = Color.Red,
+ subtitleContentColor = Color.Red,
+ content = content
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun largeTopAppBar_scrolledColorsWithCustomTitleAndWithoutSubtitleTextColor() {
+ val content =
+ @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+ LargeTopAppBar(
+ modifier = Modifier.testTag(TopAppBarTestTag),
+ title = {
+ Text(text = "Title", Modifier.testTag(TitleTestTag), color = Color.Red)
+ },
+ subtitle = null,
+ scrollBehavior = scrollBehavior,
+ )
+ }
+ assertMediumOrLargeScrolledColors(
+ appBarMaxHeight = TopAppBarDefaults.LargeAppBarWithoutSubtitleExpandedHeight,
+ appBarMinHeight = TopAppBarSmallTokens.ContainerHeight,
+ titleContentColor = Color.Red,
+ subtitleContentColor = Color.Unspecified,
content = content
)
}
@@ -1591,6 +1801,7 @@
* @param appBarMaxHeight the max height of the app bar [content]
* @param appBarMinHeight the min height of the app bar [content]
* @param titleContentColor text content color expected for the app bar's title.
+ * @param subtitleContentColor text content color expected for the app bar's subtitle.
* @param content a Composable that adds a MediumTopAppBar or a LargeTopAppBar
*/
@OptIn(ExperimentalMaterial3Api::class)
@@ -1599,6 +1810,7 @@
appBarMaxHeight: Dp,
appBarMinHeight: Dp,
titleContentColor: Color,
+ subtitleContentColor: Color,
content: @Composable (TopAppBarScrollBehavior?) -> Unit
) {
// Note: This value is specifically picked to avoid precision issues when asserting the
@@ -1608,6 +1820,7 @@
var fullyCollapsedContainerColor: Color = Color.Unspecified
var expandedAppBarBackgroundColor: Color = Color.Unspecified
var titleColor = titleContentColor
+ var subtitleColor = subtitleContentColor
lateinit var scrollBehavior: TopAppBarScrollBehavior
rule.setMaterialContent(lightColorScheme()) {
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
@@ -1623,6 +1836,9 @@
if (titleColor == Color.Unspecified) {
titleColor = TopAppBarDefaults.mediumTopAppBarColors().titleContentColor
}
+ if (subtitleColor == Color.Unspecified) {
+ subtitleColor = TopAppBarDefaults.mediumTopAppBarColors().titleContentColor
+ }
with(LocalDensity.current) {
fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
@@ -1685,13 +1901,15 @@
* @param appBarMaxHeight the max height of the app bar [content]
* @param appBarMinHeight the min height of the app bar [content]
* @param content a Composable that adds a MediumTopAppBar or a LargeTopAppBar
+ * @param withSubtitle whether a subtitle is present
*/
@OptIn(ExperimentalMaterial3Api::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
private fun assertMediumOrLargeScrolledSemantics(
appBarMaxHeight: Dp,
appBarMinHeight: Dp,
- content: @Composable (TopAppBarScrollBehavior?) -> Unit
+ content: @Composable (TopAppBarScrollBehavior?) -> Unit,
+ withSubtitle: Boolean
) {
val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
val oneThirdCollapsedOffsetDp = fullyCollapsedOffsetDp / 3
@@ -1711,6 +1929,9 @@
// Asserting that only one semantic title node is returned after the clearAndSetSemantics is
// applied to the merged tree according to the alpha values of the titles.
assertSingleTitleSemanticNode()
+ if (withSubtitle) {
+ assertSingleSubtitleSemanticNode()
+ }
// Simulate 1/3 collapsed content.
rule.runOnIdle {
@@ -1721,6 +1942,9 @@
// Assert that only one semantic title node is available while scrolling the app bar.
assertSingleTitleSemanticNode()
+ if (withSubtitle) {
+ assertSingleSubtitleSemanticNode()
+ }
// Simulate fully collapsed content.
rule.runOnIdle {
@@ -1731,6 +1955,9 @@
// Assert that only one semantic title node is available.
assertSingleTitleSemanticNode()
+ if (withSubtitle) {
+ assertSingleSubtitleSemanticNode()
+ }
}
/** Asserts that only one semantic node exists at app bar title when the tree is merged. */
@@ -1742,6 +1969,15 @@
mergedTitleNodes.assertCountEquals(1)
}
+ /** Asserts that only one semantic node exists at app bar subtitle when the tree is merged. */
+ private fun assertSingleSubtitleSemanticNode() {
+ val unmergedSubtitleNodes = rule.onAllNodesWithTag(SubtitleTestTag, useUnmergedTree = true)
+ unmergedSubtitleNodes.assertCountEquals(2)
+
+ val mergedSubtitleNodes = rule.onAllNodesWithTag(SubtitleTestTag, useUnmergedTree = false)
+ mergedSubtitleNodes.assertCountEquals(1)
+ }
+
/**
* An [IconButton] with an [Icon] inside for testing positions.
*
@@ -1775,5 +2011,6 @@
private val BottomAppBarTestTag = "bottomAppBar"
private val NavigationIconTestTag = "navigationIcon"
private val TitleTestTag = "title"
+ private val SubtitleTestTag = "subtitle"
private val ActionsTestTag = "actions"
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt
index e2cb589..c934995 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt
@@ -53,6 +53,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.testutils.assertIsEqualTo
import androidx.compose.testutils.assertPixels
import androidx.compose.testutils.assertShape
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -86,6 +87,7 @@
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.click
+import androidx.compose.ui.test.getBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithTag
@@ -105,6 +107,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -119,7 +122,6 @@
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalMaterial3Api::class)
@MediumTest
@RunWith(AndroidJUnit4::class)
class TextFieldTest {
@@ -175,6 +177,35 @@
}
@Test
+ fun testTextField_heightDoesNotChange_duringFocusAnimation() {
+ val numTicks = 5
+ val tick = TextFieldAnimationDuration / numTicks
+ rule.mainClock.autoAdvance = false
+
+ rule.setMaterialContent(lightColorScheme()) {
+ TextField(
+ modifier = Modifier.testTag(TextFieldTag),
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ )
+ }
+
+ // click to focus
+ rule.onNodeWithTag(TextFieldTag).performClick()
+
+ repeat(numTicks + 1) {
+ rule
+ .onNodeWithTag(TextFieldTag)
+ .getBoundsInRoot()
+ .height
+ .assertIsEqualTo(ExpectedDefaultTextFieldHeight)
+
+ rule.mainClock.advanceTimeBy(tick.toLong())
+ }
+ }
+
+ @Test
fun testTextFields_singleFocus() {
val textField1Tag = "TextField1"
val textField2Tag = "TextField2"
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index accd46e..1018ffb 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -53,6 +53,7 @@
import androidx.compose.material3.tokens.TopAppBarMediumTokens
import androidx.compose.material3.tokens.TopAppBarSmallCenteredTokens
import androidx.compose.material3.tokens.TopAppBarSmallTokens
+import androidx.compose.material3.tokens.TypographyKeyTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
@@ -191,6 +192,7 @@
* work in conjunction with a scrolled content to change the top app bar appearance as the content
* scrolls. See [TopAppBarScrollBehavior.nestedScrollConnection].
*/
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@ExperimentalMaterial3Api
@Composable
fun TopAppBar(
@@ -207,7 +209,9 @@
modifier = modifier,
title = title,
titleTextStyle = TopAppBarSmallTokens.HeadlineFont.value,
- centeredTitle = false,
+ subtitle = {},
+ subtitleTextStyle = TextStyle.Default,
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Start,
navigationIcon = navigationIcon,
actions = actions,
expandedHeight =
@@ -313,6 +317,7 @@
* work in conjunction with a scrolled content to change the top app bar appearance as the content
* scrolls. See [TopAppBarScrollBehavior.nestedScrollConnection].
*/
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@ExperimentalMaterial3Api
@Composable
fun CenterAlignedTopAppBar(
@@ -329,7 +334,80 @@
modifier = modifier,
title = title,
titleTextStyle = TopAppBarSmallTokens.HeadlineFont.value,
- centeredTitle = true,
+ subtitle = {},
+ subtitleTextStyle = TextStyle.Default,
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Center,
+ navigationIcon = navigationIcon,
+ actions = actions,
+ expandedHeight =
+ if (expandedHeight == Dp.Unspecified || expandedHeight == Dp.Infinity) {
+ TopAppBarDefaults.TopAppBarExpandedHeight
+ } else {
+ expandedHeight
+ },
+ windowInsets = windowInsets,
+ colors = colors,
+ scrollBehavior = scrollBehavior
+ )
+
+/**
+ * <a href="https://m3.material.io/components/top-app-bar/overview" class="external"
+ * target="_blank">Material Design small top app bar</a>.
+ *
+ * Top app bars display information and actions at the top of a screen.
+ *
+ * This small TopAppBar has slots for a title, subtitle, navigation icon, and actions.
+ *
+ * 
+ *
+ * A top app bar that uses a [scrollBehavior] to customize its nested scrolling behavior when
+ * working in conjunction with a scrolling content looks like:
+ *
+ * @sample androidx.compose.material3.samples.SimpleTopAppBarWithSubtitle
+ * @sample androidx.compose.material3.samples.SimpleCenterAlignedTopAppBarWithSubtitle
+ * @param title the title to be displayed in the top app bar
+ * @param subtitle the subtitle to be displayed in the top app bar
+ * @param modifier the [Modifier] to be applied to this top app bar
+ * @param navigationIcon the navigation icon displayed at the start of the top app bar. This should
+ * typically be an [IconButton] or [IconToggleButton].
+ * @param actions the actions displayed at the end of the top app bar. This should typically be
+ * [IconButton]s. The default layout here is a [Row], so icons inside will be placed horizontally.
+ * @param titleHorizontalAlignment the horizontal alignment of the title and subtitle
+ * @param expandedHeight this app bar's height. When a specified [scrollBehavior] causes the app bar
+ * to collapse or expand, this value will represent the maximum height that the bar will be
+ * allowed to expand. This value must be specified and finite, otherwise it will be ignored and
+ * replaced with [TopAppBarDefaults.TopAppBarExpandedHeight].
+ * @param windowInsets a window insets that app bar will respect.
+ * @param colors [TopAppBarColors] that will be used to resolve the colors used for this top app bar
+ * in different states. See [TopAppBarDefaults.topAppBarColors].
+ * @param scrollBehavior a [TopAppBarScrollBehavior] which holds various offset values that will be
+ * applied by this top app bar to set up its height and colors. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the top app bar appearance as the content
+ * scrolls. See [TopAppBarScrollBehavior.nestedScrollConnection].
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+fun TopAppBar(
+ title: @Composable () -> Unit,
+ subtitle: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ navigationIcon: @Composable () -> Unit = {},
+ actions: @Composable RowScope.() -> Unit = {},
+ titleHorizontalAlignment: TopAppBarTitleAlignment = TopAppBarTitleAlignment.Start,
+ expandedHeight: Dp = TopAppBarDefaults.TopAppBarExpandedHeight,
+ windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
+ colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
+ scrollBehavior: TopAppBarScrollBehavior? = null
+) =
+ SingleRowTopAppBar(
+ modifier = modifier,
+ title = title,
+ titleTextStyle = TopAppBarSmallTokens.HeadlineFont.value,
+ subtitle = subtitle,
+ subtitleTextStyle = TypographyKeyTokens.LabelMedium.value, // TODO tokens
+ titleHorizontalAlignment = titleHorizontalAlignment,
navigationIcon = navigationIcon,
actions = actions,
expandedHeight =
@@ -447,6 +525,7 @@
* @throws IllegalArgumentException if the provided [expandedHeight] is smaller than the
* [collapsedHeight]
*/
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@ExperimentalMaterial3Api
@Composable
fun MediumTopAppBar(
@@ -467,6 +546,11 @@
smallTitleTextStyle = TopAppBarSmallTokens.HeadlineFont.value,
titleBottomPadding = MediumTitleBottomPadding,
smallTitle = title,
+ subtitle = {},
+ subtitleTextStyle = TextStyle.Default,
+ smallSubtitle = {},
+ smallSubtitleTextStyle = TextStyle.Default,
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Start,
navigationIcon = navigationIcon,
actions = actions,
collapsedHeight =
@@ -488,6 +572,109 @@
/**
* <a href="https://m3.material.io/components/top-app-bar/overview" class="external"
+ * target="_blank">Material Design medium top app bar</a>.
+ *
+ * Top app bars display information and actions at the top of a screen.
+ *
+ * 
+ *
+ * This MediumTopAppBar has slots for a title, subtitle, navigation icon, and actions. In its
+ * default expanded state, the title and subtitle are displayed in a second row under the navigation
+ * and actions.
+ *
+ * A medium top app bar that uses a [scrollBehavior] to customize its nested scrolling behavior when
+ * working in conjunction with scrolling content looks like:
+ *
+ * @sample androidx.compose.material3.samples.ExitUntilCollapsedCenterAlignedMediumTopAppBarWithSubtitle
+ * @param title the title to be displayed in the top app bar. This title will be used in the app
+ * bar's expanded and collapsed states, although in its collapsed state it will be composed with a
+ * smaller sized [TextStyle]
+ * @param subtitle the subtitle to be displayed in the top app bar. This subtitle will be used in
+ * the app bar's expanded and collapsed states
+ * @param modifier the [Modifier] to be applied to this top app bar
+ * @param navigationIcon the navigation icon displayed at the start of the top app bar. This should
+ * typically be an [IconButton] or [IconToggleButton].
+ * @param actions the actions displayed at the end of the top app bar. This should typically be
+ * [IconButton]s. The default layout here is a [Row], so icons inside will be placed horizontally.
+ * @param titleHorizontalAlignment the horizontal alignment of the title and subtitle
+ * @param collapsedHeight this app bar height when collapsed by a provided [scrollBehavior]. This
+ * value must be specified and finite, otherwise it will be ignored and replaced with
+ * [TopAppBarDefaults.MediumAppBarCollapsedHeight].
+ * @param expandedHeight this app bar's maximum height. When a specified [scrollBehavior] causes the
+ * app bar to collapse or expand, this value will represent the maximum height that the app-bar
+ * will be allowed to expand. The expanded height is expected to be greater or equal to the
+ * [collapsedHeight], and the function will throw an [IllegalArgumentException] otherwise. Also,
+ * this value must be specified and finite, otherwise it will be ignored and replaced with
+ * [TopAppBarDefaults.MediumAppBarExpandedHeight].
+ * @param windowInsets a window insets that app bar will respect.
+ * @param colors [TopAppBarColors] that will be used to resolve the colors used for this top app bar
+ * in different states. See [TopAppBarDefaults.mediumTopAppBarColors].
+ * @param scrollBehavior a [TopAppBarScrollBehavior] which holds various offset values that will be
+ * applied by this top app bar to set up its height and colors. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the top app bar appearance as the content
+ * scrolls. See [TopAppBarScrollBehavior.nestedScrollConnection].
+ * @throws IllegalArgumentException if the provided [expandedHeight] is smaller than the
+ * [collapsedHeight]
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+fun MediumTopAppBar(
+ title: @Composable () -> Unit,
+ subtitle: (@Composable () -> Unit)?,
+ modifier: Modifier = Modifier,
+ navigationIcon: @Composable () -> Unit = {},
+ actions: @Composable RowScope.() -> Unit = {},
+ titleHorizontalAlignment: TopAppBarTitleAlignment = TopAppBarTitleAlignment.Start,
+ collapsedHeight: Dp = TopAppBarDefaults.MediumAppBarCollapsedHeight,
+ expandedHeight: Dp =
+ if (subtitle != null) {
+ TopAppBarDefaults.MediumAppBarWithSubtitleExpandedHeight
+ } else {
+ TopAppBarDefaults.MediumAppBarWithoutSubtitleExpandedHeight
+ },
+ windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
+ colors: TopAppBarColors = TopAppBarDefaults.mediumTopAppBarColors(),
+ scrollBehavior: TopAppBarScrollBehavior? = null
+) =
+ TwoRowsTopAppBar(
+ modifier = modifier,
+ title = title,
+ titleTextStyle = TypographyKeyTokens.HeadlineMedium.value, // TODO tokens
+ smallTitleTextStyle = TopAppBarSmallTokens.HeadlineFont.value,
+ titleBottomPadding = MediumTitleBottomPadding,
+ smallTitle = title,
+ subtitle = subtitle ?: {},
+ subtitleTextStyle = TypographyKeyTokens.LabelLarge.value, // TODO tokens
+ smallSubtitle = subtitle ?: {},
+ smallSubtitleTextStyle = TypographyKeyTokens.LabelMedium.value, // TODO tokens
+ titleHorizontalAlignment = titleHorizontalAlignment,
+ navigationIcon = navigationIcon,
+ actions = actions,
+ collapsedHeight =
+ if (collapsedHeight == Dp.Unspecified || collapsedHeight == Dp.Infinity) {
+ TopAppBarDefaults.MediumAppBarCollapsedHeight
+ } else {
+ collapsedHeight
+ },
+ expandedHeight =
+ if (expandedHeight == Dp.Unspecified || expandedHeight == Dp.Infinity) {
+ if (subtitle != null) {
+ TopAppBarDefaults.MediumAppBarWithSubtitleExpandedHeight
+ } else {
+ TopAppBarDefaults.MediumAppBarWithoutSubtitleExpandedHeight
+ }
+ } else {
+ expandedHeight
+ },
+ windowInsets = windowInsets,
+ colors = colors,
+ scrollBehavior = scrollBehavior
+ )
+
+/**
+ * <a href="https://m3.material.io/components/top-app-bar/overview" class="external"
* target="_blank">Material Design large top app bar</a>.
*
* Top app bars display information and actions at the top of a screen.
@@ -590,6 +777,7 @@
* @throws IllegalArgumentException if the provided [expandedHeight] is smaller to the
* [collapsedHeight]
*/
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@ExperimentalMaterial3Api
@Composable
fun LargeTopAppBar(
@@ -610,6 +798,11 @@
titleBottomPadding = LargeTitleBottomPadding,
smallTitle = title,
modifier = modifier,
+ subtitle = {},
+ subtitleTextStyle = TextStyle.Default,
+ smallSubtitle = {},
+ smallSubtitleTextStyle = TextStyle.Default,
+ titleHorizontalAlignment = TopAppBarTitleAlignment.Start,
navigationIcon = navigationIcon,
actions = actions,
collapsedHeight =
@@ -630,6 +823,109 @@
)
/**
+ * <a href="https://m3.material.io/components/top-app-bar/overview" class="external"
+ * target="_blank">Material Design large top app bar</a>.
+ *
+ * Top app bars display information and actions at the top of a screen.
+ *
+ * 
+ *
+ * This LargeTopAppBar has slots for a title, subtitle, navigation icon, and actions. In its default
+ * expanded state, the title and subtitle are displayed in a second row under the navigation and
+ * actions.
+ *
+ * A large top app bar that uses a [scrollBehavior] to customize its nested scrolling behavior when
+ * working in conjunction with scrolling content looks like:
+ *
+ * @sample androidx.compose.material3.samples.ExitUntilCollapsedCenterAlignedLargeTopAppBarWithSubtitle
+ * @param title the title to be displayed in the top app bar. This title will be used in the app
+ * bar's expanded and collapsed states, although in its collapsed state it will be composed with a
+ * smaller sized [TextStyle]
+ * @param subtitle the subtitle to be displayed in the top app bar. This subtitle will be used in
+ * the app bar's expanded and collapsed states
+ * @param modifier the [Modifier] to be applied to this top app bar
+ * @param navigationIcon the navigation icon displayed at the start of the top app bar. This should
+ * typically be an [IconButton] or [IconToggleButton].
+ * @param actions the actions displayed at the end of the top app bar. This should typically be
+ * [IconButton]s. The default layout here is a [Row], so icons inside will be placed horizontally.
+ * @param titleHorizontalAlignment the horizontal alignment of the title and subtitle
+ * @param collapsedHeight this app bar height when collapsed by a provided [scrollBehavior]. This
+ * value must be specified and finite, otherwise it will be ignored and replaced with
+ * [TopAppBarDefaults.LargeAppBarCollapsedHeight].
+ * @param expandedHeight this app bar's maximum height. When a specified [scrollBehavior] causes the
+ * app bar to collapse or expand, this value will represent the maximum height that the app-bar
+ * will be allowed to expand. The expanded height is expected to be greater or equal to the
+ * [collapsedHeight], and the function will throw an [IllegalArgumentException] otherwise. Also,
+ * this value must be specified and finite, otherwise it will be ignored and replaced with
+ * [TopAppBarDefaults.LargeAppBarExpandedHeight].
+ * @param windowInsets a window insets that app bar will respect.
+ * @param colors [TopAppBarColors] that will be used to resolve the colors used for this top app bar
+ * in different states. See [TopAppBarDefaults.largeTopAppBarColors].
+ * @param scrollBehavior a [TopAppBarScrollBehavior] which holds various offset values that will be
+ * applied by this top app bar to set up its height and colors. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the top app bar appearance as the content
+ * scrolls. See [TopAppBarScrollBehavior.nestedScrollConnection].
+ * @throws IllegalArgumentException if the provided [expandedHeight] is smaller to the
+ * [collapsedHeight]
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+fun LargeTopAppBar(
+ title: @Composable () -> Unit,
+ subtitle: (@Composable () -> Unit)?,
+ modifier: Modifier = Modifier,
+ navigationIcon: @Composable () -> Unit = {},
+ actions: @Composable RowScope.() -> Unit = {},
+ titleHorizontalAlignment: TopAppBarTitleAlignment = TopAppBarTitleAlignment.Start,
+ collapsedHeight: Dp = TopAppBarDefaults.LargeAppBarCollapsedHeight,
+ expandedHeight: Dp =
+ if (subtitle != null) {
+ TopAppBarDefaults.LargeAppBarWithSubtitleExpandedHeight
+ } else {
+ TopAppBarDefaults.LargeAppBarWithoutSubtitleExpandedHeight
+ },
+ windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
+ colors: TopAppBarColors = TopAppBarDefaults.largeTopAppBarColors(),
+ scrollBehavior: TopAppBarScrollBehavior? = null
+) =
+ TwoRowsTopAppBar(
+ title = title,
+ titleTextStyle = TypographyKeyTokens.DisplaySmall.value, // TODO tokens
+ smallTitleTextStyle = TopAppBarSmallTokens.HeadlineFont.value,
+ titleBottomPadding = LargeTitleBottomPadding,
+ smallTitle = title,
+ modifier = modifier,
+ subtitle = subtitle ?: {},
+ subtitleTextStyle = TypographyKeyTokens.TitleMedium.value, // TODO tokens
+ smallSubtitle = subtitle ?: {},
+ smallSubtitleTextStyle = TypographyKeyTokens.LabelMedium.value, // TODO tokens
+ titleHorizontalAlignment = titleHorizontalAlignment,
+ navigationIcon = navigationIcon,
+ actions = actions,
+ collapsedHeight =
+ if (collapsedHeight == Dp.Unspecified || collapsedHeight == Dp.Infinity) {
+ TopAppBarDefaults.LargeAppBarCollapsedHeight
+ } else {
+ collapsedHeight
+ },
+ expandedHeight =
+ if (expandedHeight == Dp.Unspecified || expandedHeight == Dp.Infinity) {
+ if (subtitle != null) {
+ TopAppBarDefaults.LargeAppBarWithSubtitleExpandedHeight
+ } else {
+ TopAppBarDefaults.LargeAppBarWithoutSubtitleExpandedHeight
+ }
+ } else {
+ expandedHeight
+ },
+ windowInsets = windowInsets,
+ colors = colors,
+ scrollBehavior = scrollBehavior
+ )
+
+/**
* <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external"
* target="_blank">Material Design bottom app bar</a>.
*
@@ -1259,11 +1555,23 @@
/** The default expanded height of a [MediumTopAppBar]. */
val MediumAppBarExpandedHeight: Dp = TopAppBarMediumTokens.ContainerHeight
+ /** The default expanded height of a [MediumTopAppBar] without subtitle. */
+ val MediumAppBarWithoutSubtitleExpandedHeight: Dp = 104.dp // TODO tokens
+
+ /** The default expanded height of a [MediumTopAppBar] with subtitle. */
+ val MediumAppBarWithSubtitleExpandedHeight: Dp = 124.dp // TODO tokens
+
/** The default height of a [LargeTopAppBar] when collapsed by a [TopAppBarScrollBehavior]. */
val LargeAppBarCollapsedHeight: Dp = TopAppBarSmallTokens.ContainerHeight
/** The default expanded height of a [LargeTopAppBar]. */
val LargeAppBarExpandedHeight: Dp = TopAppBarLargeTokens.ContainerHeight
+
+ /** The default expanded height of a [LargeTopAppBar] without subtitle. */
+ val LargeAppBarWithoutSubtitleExpandedHeight: Dp = 120.dp // TODO tokens
+
+ /** The default expanded height of a [LargeTopAppBar] with subtitle. */
+ val LargeAppBarWithSubtitleExpandedHeight: Dp = 144.dp // TODO tokens
}
/**
@@ -1476,6 +1784,22 @@
}
}
+/** This class defines ways title and subtitle can be aligned along a TopAppBar's main axis. */
+@ExperimentalMaterial3ExpressiveApi
+@JvmInline
+value class TopAppBarTitleAlignment private constructor(internal val value: Int) {
+ companion object {
+ /** Start align the title and subtitle if present */
+ val Start = TopAppBarTitleAlignment(-1)
+
+ /** Center align the title and subtitle if present */
+ val Center = TopAppBarTitleAlignment(0)
+
+ /** End align the title and subtitle if present */
+ val End = TopAppBarTitleAlignment(1)
+ }
+}
+
/**
* A BottomAppBarScrollBehavior defines how a bottom app bar should behave when the content under it
* is scrolled.
@@ -1835,17 +2159,17 @@
* A single-row top app bar that is designed to be called by the small and center aligned top app
* bar composables.
*
- * This SingleRowTopAppBar has slots for a title, navigation icon, and actions. When the
- * [centeredTitle] flag is true, the title will be horizontally aligned to the center of the top app
- * bar width.
+ * This SingleRowTopAppBar has slots for a title, subtitle, navigation icon, and actions.
*/
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun SingleRowTopAppBar(
modifier: Modifier = Modifier,
title: @Composable () -> Unit,
titleTextStyle: TextStyle,
- centeredTitle: Boolean,
+ subtitle: @Composable () -> Unit,
+ subtitleTextStyle: TextStyle,
+ titleHorizontalAlignment: TopAppBarTitleAlignment,
navigationIcon: @Composable () -> Unit,
actions: @Composable RowScope.() -> Unit,
expandedHeight: Dp,
@@ -1938,10 +2262,11 @@
actionIconContentColor = colors.actionIconContentColor,
title = title,
titleTextStyle = titleTextStyle,
+ subtitle = subtitle,
+ subtitleTextStyle = subtitleTextStyle,
titleAlpha = { 1f },
titleVerticalArrangement = Arrangement.Center,
- titleHorizontalArrangement =
- if (centeredTitle) Arrangement.Center else Arrangement.Start,
+ titleHorizontalAlignment = titleHorizontalAlignment,
titleBottomPadding = 0,
hideTitleSemantics = false,
navigationIcon = navigationIcon,
@@ -1954,7 +2279,7 @@
* A two-rows top app bar that is designed to be called by the Large and Medium top app bar
* composables.
*/
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun TwoRowsTopAppBar(
modifier: Modifier = Modifier,
@@ -1963,6 +2288,11 @@
titleBottomPadding: Dp,
smallTitle: @Composable () -> Unit,
smallTitleTextStyle: TextStyle,
+ subtitle: @Composable () -> Unit,
+ subtitleTextStyle: TextStyle,
+ smallSubtitle: @Composable () -> Unit,
+ smallSubtitleTextStyle: TextStyle,
+ titleHorizontalAlignment: TopAppBarTitleAlignment,
navigationIcon: @Composable () -> Unit,
actions: @Composable RowScope.() -> Unit,
collapsedHeight: Dp,
@@ -2059,9 +2389,11 @@
actionIconContentColor = colors.actionIconContentColor,
title = smallTitle,
titleTextStyle = smallTitleTextStyle,
+ subtitle = smallSubtitle,
+ subtitleTextStyle = smallSubtitleTextStyle,
titleAlpha = topTitleAlpha,
titleVerticalArrangement = Arrangement.Center,
- titleHorizontalArrangement = Arrangement.Start,
+ titleHorizontalAlignment = titleHorizontalAlignment,
titleBottomPadding = 0,
hideTitleSemantics = hideTopRowSemantics,
navigationIcon = navigationIcon,
@@ -2082,9 +2414,11 @@
actionIconContentColor = colors.actionIconContentColor,
title = title,
titleTextStyle = titleTextStyle,
+ subtitle = subtitle,
+ subtitleTextStyle = subtitleTextStyle,
titleAlpha = bottomTitleAlpha,
titleVerticalArrangement = Arrangement.Bottom,
- titleHorizontalArrangement = Arrangement.Start,
+ titleHorizontalAlignment = titleHorizontalAlignment,
titleBottomPadding = titleBottomPaddingPx,
hideTitleSemantics = hideBottomRowSemantics,
navigationIcon = {},
@@ -2112,7 +2446,7 @@
* @param titleTextStyle the title's text style
* @param titleAlpha the title's alpha
* @param titleVerticalArrangement the title's vertical arrangement
- * @param titleHorizontalArrangement the title's horizontal arrangement
+ * @param titleHorizontalAlignment the title's horizontal alignment
* @param titleBottomPadding the title's bottom padding
* @param hideTitleSemantics hides the title node from the semantic tree. Apply this boolean when
* this layout is part of a [TwoRowsTopAppBar] to hide the title's semantics from accessibility
@@ -2121,6 +2455,7 @@
* @param navigationIcon a navigation icon [Composable]
* @param actions actions [Composable]
*/
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun TopAppBarLayout(
modifier: Modifier,
@@ -2130,9 +2465,11 @@
actionIconContentColor: Color,
title: @Composable () -> Unit,
titleTextStyle: TextStyle,
+ subtitle: @Composable () -> Unit,
+ subtitleTextStyle: TextStyle,
titleAlpha: () -> Float,
titleVerticalArrangement: Arrangement.Vertical,
- titleHorizontalArrangement: Arrangement.Horizontal,
+ titleHorizontalAlignment: TopAppBarTitleAlignment,
titleBottomPadding: Int,
hideTitleSemantics: Boolean,
navigationIcon: @Composable () -> Unit,
@@ -2146,17 +2483,31 @@
content = navigationIcon
)
}
- Box(
- Modifier.layoutId("title")
- .padding(horizontal = TopAppBarHorizontalPadding)
- .then(if (hideTitleSemantics) Modifier.clearAndSetSemantics {} else Modifier)
- .graphicsLayer { alpha = titleAlpha() }
+ Column(
+ modifier =
+ Modifier.layoutId("title")
+ .padding(horizontal = TopAppBarHorizontalPadding)
+ .then(
+ if (hideTitleSemantics) Modifier.clearAndSetSemantics {} else Modifier
+ )
+ .graphicsLayer { alpha = titleAlpha() },
+ horizontalAlignment =
+ when (titleHorizontalAlignment) {
+ TopAppBarTitleAlignment.Start -> Alignment.Start
+ TopAppBarTitleAlignment.Center -> Alignment.CenterHorizontally
+ else -> Alignment.End
+ }
) {
ProvideContentColorTextStyle(
contentColor = titleContentColor,
textStyle = titleTextStyle,
content = title
)
+ ProvideContentColorTextStyle(
+ contentColor = titleContentColor,
+ textStyle = subtitleTextStyle,
+ content = subtitle
+ )
}
Box(Modifier.layoutId("actionIcons").padding(end = TopAppBarHorizontalPadding)) {
CompositionLocalProvider(
@@ -2218,31 +2569,29 @@
// Title
titlePlaceable.placeRelative(
x =
- when (titleHorizontalArrangement) {
- Arrangement.Center -> {
+ when (titleHorizontalAlignment) {
+ TopAppBarTitleAlignment.Center -> {
var baseX = (constraints.maxWidth - titlePlaceable.width) / 2
if (baseX < navigationIconPlaceable.width) {
// May happen if the navigation is wider than the actions and the
// title is long. In this case, prioritize showing more of the title
- // by
- // offsetting it to the right.
+ // by offsetting it to the right.
baseX += (navigationIconPlaceable.width - baseX)
} else if (
baseX + titlePlaceable.width >
constraints.maxWidth - actionIconsPlaceable.width
) {
// May happen if the actions are wider than the navigation and the
- // title
- // is long. In this case, offset to the left.
+ // title is long. In this case, offset to the left.
baseX +=
((constraints.maxWidth - actionIconsPlaceable.width) -
(baseX + titlePlaceable.width))
}
baseX
}
- Arrangement.End ->
+ TopAppBarTitleAlignment.End ->
constraints.maxWidth - titlePlaceable.width - actionIconsPlaceable.width
- // Arrangement.Start.
+ // TopAppBarTitleAlignment.Start
// An TopAppBarTitleInset will make sure the title is offset in case the
// navigation icon is missing.
else -> max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width)
@@ -2251,8 +2600,7 @@
when (titleVerticalArrangement) {
Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2
// Apply bottom padding from the title's baseline only when the Arrangement
- // is
- // "Bottom".
+ // is "Bottom".
Arrangement.Bottom ->
if (titleBottomPadding == 0) {
layoutHeight - titlePlaceable.height
@@ -2262,8 +2610,7 @@
val paddingFromBottom =
titleBottomPadding - (titlePlaceable.height - titleBaseline)
// Adjust the bottom padding to a smaller number if there is no room
- // to
- // fit the title.
+ // to fit the title.
val heightWithPadding = paddingFromBottom + titlePlaceable.height
val adjustedBottomPadding =
if (heightWithPadding > constraints.maxHeight) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
index 5f56c37..0675d1c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
@@ -18,6 +18,8 @@
import androidx.compose.material3.internal.toPath
import androidx.compose.material3.internal.transformed
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.center
@@ -43,39 +45,46 @@
import kotlin.math.sin
/**
- * Returns a normalized [Path] for this [RoundedPolygon].
+ * Returns a normalized [Path] that is remembered across compositions for this [RoundedPolygon].
*
- * @param path a [Path] object which, if supplied, will avoid the function having to create a new
- * [Path] object
* @param startAngle an angle to rotate the Material shape's path to start drawing from. The
* rotation pivot is set to be the shape's centerX and centerY coordinates.
* @see RoundedPolygon.normalized
*/
@ExperimentalMaterial3ExpressiveApi
-fun RoundedPolygon.toPath(path: Path = Path(), startAngle: Int = 0): Path {
- return this.toPath(path = path, startAngle = startAngle, repeatPath = false, closePath = true)
+@Composable
+fun RoundedPolygon.toPath(startAngle: Int = 0): Path {
+ val path = remember { Path() }
+ return remember(this, startAngle) {
+ this.toPath(path = path, startAngle = startAngle, repeatPath = false, closePath = true)
+ }
}
/**
- * Returns a [Shape] for this [RoundedPolygon].
+ * Returns a [Shape] that is remembered across compositions for this [RoundedPolygon].
*
* @param startAngle an angle to rotate the Material shape's path to start drawing from. The
* rotation pivot is always set to be the shape's centerX and centerY coordinates.
*/
@ExperimentalMaterial3ExpressiveApi
+@Composable
fun RoundedPolygon.toShape(startAngle: Int = 0): Shape {
- return object : Shape {
- override fun createOutline(
- size: Size,
- layoutDirection: LayoutDirection,
- density: Density
- ): Outline {
- val path = toPath(startAngle = startAngle)
- val scaleMatrix = Matrix().apply { scale(x = size.width, y = size.height) }
- // Scale and translate the path to align its center with the available size center.
- path.transform(scaleMatrix)
- path.translate(size.center - path.getBounds().center)
- return Outline.Generic(path)
+ return remember(this, startAngle) {
+ object : Shape {
+ private val path: Path = toPath(startAngle = startAngle)
+
+ override fun createOutline(
+ size: Size,
+ layoutDirection: LayoutDirection,
+ density: Density
+ ): Outline {
+ val scaleMatrix = Matrix().apply { scale(x = size.width, y = size.height) }
+ // Scale and translate the path to align its center with the available size
+ // center.
+ path.transform(scaleMatrix)
+ path.translate(size.center - path.getBounds().center)
+ return Outline.Generic(path)
+ }
}
}
}
@@ -110,75 +119,146 @@
private val rotateNeg135 = Matrix().apply { rotateZ(-135f) }
private val unrounded = CornerRounding.Unrounded
- val Circle: RoundedPolygon by lazy { circle().normalized() }
+ private var _circle: RoundedPolygon? = null
+ private var _square: RoundedPolygon? = null
+ private var _slanted: RoundedPolygon? = null
+ private var _arch: RoundedPolygon? = null
+ private var _fan: RoundedPolygon? = null
+ private var _arrow: RoundedPolygon? = null
+ private var _semiCircle: RoundedPolygon? = null
+ private var _oval: RoundedPolygon? = null
+ private var _pill: RoundedPolygon? = null
+ private var _triangle: RoundedPolygon? = null
+ private var _diamond: RoundedPolygon? = null
+ private var _clamShell: RoundedPolygon? = null
+ private var _pentagon: RoundedPolygon? = null
+ private var _gem: RoundedPolygon? = null
+ private var _verySunny: RoundedPolygon? = null
+ private var _sunny: RoundedPolygon? = null
+ private var _cookie4Sided: RoundedPolygon? = null
+ private var _cookie6Sided: RoundedPolygon? = null
+ private var _cookie7Sided: RoundedPolygon? = null
+ private var _cookie9Sided: RoundedPolygon? = null
+ private var _cookie12Sided: RoundedPolygon? = null
+ private var _ghostish: RoundedPolygon? = null
+ private var _clover4Leaf: RoundedPolygon? = null
+ private var _clover8Leaf: RoundedPolygon? = null
+ private var _burst: RoundedPolygon? = null
+ private var _softBurst: RoundedPolygon? = null
+ private var _boom: RoundedPolygon? = null
+ private var _softBoom: RoundedPolygon? = null
+ private var _flower: RoundedPolygon? = null
+ private var _puffy: RoundedPolygon? = null
+ private var _puffyDiamond: RoundedPolygon? = null
+ private var _pixelCircle: RoundedPolygon? = null
+ private var _pixelTriangle: RoundedPolygon? = null
+ private var _bun: RoundedPolygon? = null
+ private var _heart: RoundedPolygon? = null
- val Square: RoundedPolygon by lazy { square().normalized() }
+ val Circle
+ get() = _circle ?: circle().normalized().also { _circle = it }
- val Slanted: RoundedPolygon by lazy { slanted().normalized() }
+ val Square
+ get() = _square ?: square().normalized().also { _square = it }
- val Arch: RoundedPolygon by lazy { arch().normalized() }
+ val Slanted
+ get() = _slanted ?: slanted().normalized().also { _slanted = it }
- val Fan: RoundedPolygon by lazy { fan().normalized() }
+ val Arch
+ get() = _arch ?: arch().normalized().also { _arch = it }
- val Arrow: RoundedPolygon by lazy { arrow().normalized() }
+ val Fan
+ get() = _fan ?: fan().normalized().also { _fan = it }
- val SemiCircle: RoundedPolygon by lazy { semiCircle().normalized() }
+ val Arrow
+ get() = _arrow ?: arrow().normalized().also { _arrow = it }
- val Oval: RoundedPolygon by lazy { oval().normalized() }
+ val SemiCircle
+ get() = _semiCircle ?: semiCircle().normalized().also { _semiCircle = it }
- val Pill: RoundedPolygon by lazy { pill().normalized() }
+ val Oval
+ get() = _oval ?: oval().normalized().also { _oval = it }
- val Triangle: RoundedPolygon by lazy { triangle().normalized() }
+ val Pill
+ get() = _pill ?: pill().normalized().also { _pill = it }
- val Diamond: RoundedPolygon by lazy { diamond().normalized() }
+ val Triangle
+ get() = _triangle ?: triangle().normalized().also { _triangle = it }
- val ClamShell: RoundedPolygon by lazy { clamShell().normalized() }
+ val Diamond
+ get() = _diamond ?: diamond().normalized().also { _diamond = it }
- val Pentagon: RoundedPolygon by lazy { pentagon().normalized() }
+ val ClamShell
+ get() = _clamShell ?: clamShell().normalized().also { _clamShell = it }
- val Gem: RoundedPolygon by lazy { gem().normalized() }
+ val Pentagon
+ get() = _pentagon ?: pentagon().normalized().also { _pentagon = it }
- val VerySunny: RoundedPolygon by lazy { verySunny().normalized() }
+ val Gem
+ get() = _gem ?: gem().normalized().also { _gem = it }
- val Sunny: RoundedPolygon by lazy { sunny().normalized() }
+ val VerySunny
+ get() = _verySunny ?: verySunny().normalized().also { _verySunny = it }
- val Cookie4Sided: RoundedPolygon by lazy { cookie4().normalized() }
+ val Sunny
+ get() = _sunny ?: sunny().normalized().also { _sunny = it }
- val Cookie6Sided: RoundedPolygon by lazy { cookie6().normalized() }
+ val Cookie4Sided
+ get() = _cookie4Sided ?: cookie4().normalized().also { _cookie4Sided = it }
- val Cookie7Sided: RoundedPolygon by lazy { cookie7().normalized() }
+ val Cookie6Sided
+ get() = _cookie6Sided ?: cookie6().normalized().also { _cookie6Sided = it }
- val Cookie9Sided: RoundedPolygon by lazy { cookie9().normalized() }
+ val Cookie7Sided
+ get() = _cookie7Sided ?: cookie7().normalized().also { _cookie7Sided = it }
- val Cookie12Sided: RoundedPolygon by lazy { cookie12().normalized() }
+ val Cookie9Sided
+ get() = _cookie9Sided ?: cookie9().normalized().also { _cookie9Sided = it }
- val Ghostish: RoundedPolygon by lazy { ghostish().normalized() }
+ val Cookie12Sided
+ get() = _cookie12Sided ?: cookie12().normalized().also { _cookie12Sided = it }
- val Clover4Leaf: RoundedPolygon by lazy { clover4().normalized() }
+ val Ghostish
+ get() = _ghostish ?: ghostish().normalized().also { _ghostish = it }
- val Clover8Leaf: RoundedPolygon by lazy { clover8().normalized() }
+ val Clover4Leaf
+ get() = _clover4Leaf ?: clover4().normalized().also { _clover4Leaf = it }
- val Burst: RoundedPolygon by lazy { burst().normalized() }
+ val Clover8Leaf
+ get() = _clover8Leaf ?: clover8().normalized().also { _clover8Leaf = it }
- val SoftBurst: RoundedPolygon by lazy { softBurst().normalized() }
+ val Burst
+ get() = _burst ?: burst().normalized().also { _burst = it }
- val Boom: RoundedPolygon by lazy { boom().normalized() }
+ val SoftBurst
+ get() = _softBurst ?: softBurst().normalized().also { _softBurst = it }
- val SoftBoom: RoundedPolygon by lazy { softBoom().normalized() }
+ val Boom
+ get() = _boom ?: boom().normalized().also { _boom = it }
- val Flower: RoundedPolygon by lazy { flower().normalized() }
+ val SoftBoom
+ get() = _softBoom ?: softBoom().normalized().also { _softBoom = it }
- val Puffy: RoundedPolygon by lazy { puffy().normalized() }
+ val Flower
+ get() = _flower ?: flower().normalized().also { _flower = it }
- val PuffyDiamond: RoundedPolygon by lazy { puffyDiamond().normalized() }
+ val Puffy
+ get() = _puffy ?: puffy().normalized().also { _puffy = it }
- val PixelCircle: RoundedPolygon by lazy { pixelCircle().normalized() }
+ val PuffyDiamond
+ get() = _puffyDiamond ?: puffyDiamond().normalized().also { _puffyDiamond = it }
- val PixelTriangle: RoundedPolygon by lazy { pixelTriangle().normalized() }
+ val PixelCircle
+ get() = _pixelCircle ?: pixelCircle().normalized().also { _pixelCircle = it }
- val Bun: RoundedPolygon by lazy { bun().normalized() }
+ val PixelTriangle
+ get() = _pixelTriangle ?: pixelTriangle().normalized().also { _pixelTriangle = it }
- val Heart: RoundedPolygon by lazy { heart().normalized() }
+ val Bun
+ get() = _bun ?: bun().normalized().also { _bun = it }
+
+ val Heart
+ get() = _heart ?: heart().normalized().also { _heart = it }
internal fun circle(numVertices: Int = 10): RoundedPolygon {
return RoundedPolygon.circle(numVertices = numVertices)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
index 2df74dc..feb749e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
@@ -47,10 +47,12 @@
import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
@@ -700,9 +702,11 @@
interactionSource: MutableInteractionSource? = null,
content: @Composable RowScope.() -> Unit
) {
- val leftCornerMorphProgress: Float by animateFloatAsState(if (expanded) 1f else 0f)
+ val cornerMorphProgress: Float by animateFloatAsState(if (expanded) 1f else 0f)
@Suppress("NAME_SHADOWING")
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+
TrailingButton(
onClick = onClick,
modifier = modifier,
@@ -711,7 +715,7 @@
elevation = elevation,
border = border,
interactionSource = interactionSource,
- shape = rememberTrailingButtonShape { leftCornerMorphProgress },
+ shape = rememberTrailingButtonShape(isRtl) { cornerMorphProgress },
content = content,
)
}
@@ -719,25 +723,37 @@
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
-private fun rememberTrailingButtonShape(progress: () -> Float) = remember {
- GenericShape { size, _ ->
- val rect = Rect(Offset.Zero, size)
- val originalLeftCornerRadius =
- CornerRadius((size.height * InnerCornerRadiusPercentage / 100))
- val originalRoundRect =
- RoundRect(
- rect,
- originalLeftCornerRadius,
- CornerRadius(size.height / 2),
- CornerRadius(size.height / 2),
- originalLeftCornerRadius
- )
- val endRoundRect = RoundRect(rect, CornerRadius(size.height / 2))
+private fun rememberTrailingButtonShape(isRtl: Boolean, progress: () -> Float) =
+ remember(isRtl, progress) {
+ GenericShape { size, _ ->
+ val rect = Rect(Offset.Zero, size)
+ val originalStartCornerRadius =
+ CornerRadius((size.height * InnerCornerRadiusPercentage / 100))
+ val originalRoundRect =
+ if (isRtl) {
+ RoundRect(
+ rect,
+ CornerRadius(size.height / 2),
+ originalStartCornerRadius,
+ originalStartCornerRadius,
+ CornerRadius(size.height / 2)
+ )
+ } else {
+ RoundRect(
+ rect,
+ originalStartCornerRadius,
+ CornerRadius(size.height / 2),
+ CornerRadius(size.height / 2),
+ originalStartCornerRadius
+ )
+ }
- val roundRect = lerp(originalRoundRect, endRoundRect, progress.invoke())
- addRoundRect(roundRect)
+ val endRoundRect = RoundRect(rect, CornerRadius(size.height / 2))
+
+ val roundRect = lerp(originalRoundRect, endRoundRect, progress.invoke())
+ addRoundRect(roundRect)
+ }
}
-}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
index fae16bb..1f41226 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
@@ -47,7 +47,7 @@
import androidx.compose.material3.internal.SuffixId
import androidx.compose.material3.internal.SupportingId
import androidx.compose.material3.internal.TextFieldId
-import androidx.compose.material3.internal.TextFieldPadding
+import androidx.compose.material3.internal.TextFieldLabelExtraPadding
import androidx.compose.material3.internal.TrailingId
import androidx.compose.material3.internal.ZeroConstraints
import androidx.compose.material3.internal.defaultErrorSemantics
@@ -82,6 +82,7 @@
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
@@ -691,7 +692,6 @@
supportingHeight = heightOrZero(supportingPlaceable),
animationProgress = animationProgress,
constraints = constraints,
- density = density,
paddingValues = paddingValues,
)
val height = totalHeight - supportingHeight
@@ -710,6 +710,9 @@
return layout(width, totalHeight) {
if (labelPlaceable != null) {
+ // The padding defined by the user only applies to the text field when the label
+ // is focused. More padding needs to be added when the text field is unfocused.
+ val labelStartPosition = topPaddingValue + TextFieldLabelExtraPadding.roundToPx()
placeWithLabel(
width = width,
totalHeight = totalHeight,
@@ -723,10 +726,10 @@
containerPlaceable = containerPlaceable,
supportingPlaceable = supportingPlaceable,
singleLine = singleLine,
+ labelStartPosition = labelStartPosition,
labelEndPosition = topPaddingValue,
textPosition = topPaddingValue + labelPlaceable.height,
animationProgress = animationProgress,
- density = density,
)
} else {
placeWithoutLabel(
@@ -904,7 +907,6 @@
supportingHeight = supportingHeight,
animationProgress = animationProgress,
constraints = ZeroConstraints,
- density = density,
paddingValues = paddingValues
)
}
@@ -939,7 +941,7 @@
return max(wrappedWidth, constraints.minWidth)
}
-private fun calculateHeight(
+private fun Density.calculateHeight(
textFieldHeight: Int,
labelHeight: Int,
leadingHeight: Int,
@@ -950,22 +952,21 @@
supportingHeight: Int,
animationProgress: Float,
constraints: Constraints,
- density: Float,
paddingValues: PaddingValues
): Int {
val hasLabel = labelHeight > 0
- val verticalPadding =
- density *
- (paddingValues.calculateTopPadding() + paddingValues.calculateBottomPadding()).value
- // Even though the padding is defined by the developer, if there's a label, it only affects the
- // text field in the focused state. Otherwise, we use the default value.
- val actualVerticalPadding =
+ // The padding defined by the user only applies to the text field when the label
+ // is focused. More padding needs to be added when the text field is unfocused.
+ val baseVerticalPadding =
+ (paddingValues.calculateTopPadding() + paddingValues.calculateBottomPadding()).toPx()
+ val labelVerticalPadding =
if (hasLabel) {
- lerp((TextFieldPadding * 2).value * density, verticalPadding, animationProgress)
+ lerp((TextFieldLabelExtraPadding * 2).toPx(), 0f, animationProgress)
} else {
- verticalPadding
+ 0f
}
+ val verticalPadding = (baseVerticalPadding + labelVerticalPadding).roundToInt()
val inputFieldHeight =
maxOf(
@@ -977,11 +978,11 @@
)
val middleSectionHeight =
- actualVerticalPadding + lerp(0, labelHeight, animationProgress) + inputFieldHeight
+ verticalPadding + (if (animationProgress == 1f) labelHeight else 0) + inputFieldHeight
return max(
constraints.minHeight,
- maxOf(leadingHeight, trailingHeight, middleSectionHeight.roundToInt()) + supportingHeight
+ maxOf(leadingHeight, trailingHeight, middleSectionHeight) + supportingHeight
)
}
@@ -993,7 +994,7 @@
width: Int,
totalHeight: Int,
textfieldPlaceable: Placeable,
- labelPlaceable: Placeable?,
+ labelPlaceable: Placeable,
placeholderPlaceable: Placeable?,
leadingPlaceable: Placeable?,
trailingPlaceable: Placeable?,
@@ -1002,10 +1003,10 @@
containerPlaceable: Placeable,
supportingPlaceable: Placeable?,
singleLine: Boolean,
+ labelStartPosition: Int,
labelEndPosition: Int,
textPosition: Int,
animationProgress: Float,
- density: Float
) {
// place container
containerPlaceable.place(IntOffset.Zero)
@@ -1018,23 +1019,18 @@
0,
Alignment.CenterVertically.align(leadingPlaceable.height, height)
)
- labelPlaceable?.let {
- // if it's a single line, the label's start position is in the center of the
- // container. When it's a multiline text field, the label's start position is at the
- // top with padding
- val startPosition =
- if (singleLine) {
- Alignment.CenterVertically.align(it.height, height)
- } else {
- // Even though the padding is defined by the developer, it only affects the text
- // field
- // when the text field is focused. Otherwise, we use the default value.
- (TextFieldPadding.value * density).roundToInt()
- }
- val distance = startPosition - labelEndPosition
- val positionY = startPosition - (distance * animationProgress).roundToInt()
- it.placeRelative(widthOrZero(leadingPlaceable), positionY)
- }
+
+ val labelY =
+ labelPlaceable.let {
+ val startPosition =
+ if (singleLine) {
+ Alignment.CenterVertically.align(it.height, height)
+ } else {
+ labelStartPosition
+ }
+ lerp(startPosition, labelEndPosition, animationProgress)
+ }
+ labelPlaceable.placeRelative(widthOrZero(leadingPlaceable), labelY)
prefixPlaceable?.placeRelative(widthOrZero(leadingPlaceable), textPosition)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
index a7f4fb3..af7ea36 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
@@ -227,12 +227,11 @@
* @param shape defines the shape of this decoration box's container
* @param colors [TextFieldColors] that will be used to resolve the colors used for this text
* field decoration box in different states. See [TextFieldDefaults.colors].
- * @param contentPadding the padding applied between the internal elements of this decoration
- * box and the edge of its container. If a [label] is present, the top padding represents the
- * distance from the top edge of the container to the top of the label when the text field is
- * focused. When [label] is null, the top padding represents the distance from the top edge of
- * the container to the top of the input field. All other paddings represent the distance from
- * the edge of the container to the corresponding edge of the closest element.
+ * @param contentPadding the padding between the input field and the surrounding elements of the
+ * decoration box. Note that the padding values may not be respected if they are incompatible
+ * with the text field's size constraints or layout. See
+ * [TextFieldDefaults.contentPaddingWithLabel] and
+ * [TextFieldDefaults.contentPaddingWithoutLabel].
* @param container the container to be drawn behind the text field. By default, this uses
* [Container]. Default colors for the container come from the [colors].
*/
@@ -297,10 +296,12 @@
}
/**
- * Default content padding applied to [TextField] when there is a label.
+ * Default content padding of the input field within the [TextField] when there is a label,
+ * except for the top padding, which instead represents the padding of the label in the focused
+ * state. The input field is placed directly beneath the label.
*
- * The top padding represents ths distance between the top edge of the [TextField] and the top
- * of the label in the focused state. The input field is placed directly beneath the label.
+ * Horizontal padding represents the distance between the input field and the leading/trailing
+ * icons (if present) or the horizontal edges of the container if there are no icons.
*/
fun contentPaddingWithLabel(
start: Dp = TextFieldPadding,
@@ -309,7 +310,12 @@
bottom: Dp = TextFieldWithLabelVerticalPadding
): PaddingValues = PaddingValues(start, top, end, bottom)
- /** Default content padding applied to [TextField] when the label is null. */
+ /**
+ * Default content padding of the input field within the [TextField] when the label is null.
+ *
+ * Horizontal padding represents the distance between the input field and the leading/trailing
+ * icons (if present) or the horizontal edges of the container if there are no icons.
+ */
fun contentPaddingWithoutLabel(
start: Dp = TextFieldPadding,
top: Dp = TextFieldPadding,
@@ -842,8 +848,10 @@
* @param supportingText the optional supporting text to be displayed below the text field
* @param colors [TextFieldColors] that will be used to resolve the colors used for this text
* field in different states. See [OutlinedTextFieldDefaults.colors].
- * @param contentPadding the padding applied between the internal elements of this decoration
- * box and the edge of its container
+ * @param contentPadding the padding between the input field and the surrounding elements of the
+ * decoration box. Note that the padding values may not be respected if they are incompatible
+ * with the text field's size constraints or layout. See
+ * [OutlinedTextFieldDefaults.contentPadding].
* @param container the container to be drawn behind the text field. By default, this is
* transparent and only includes a border. The cutout in the border to fit the [label] will be
* automatically added by the framework. Default colors for the container come from the
@@ -904,7 +912,10 @@
}
/**
- * Default content padding applied to [OutlinedTextField]. See [PaddingValues] for more details.
+ * Default content padding of the input field within the [OutlinedTextField].
+ *
+ * Horizontal padding represents the distance between the input field and the leading/trailing
+ * icons (if present) or the horizontal edges of the container if there are no icons.
*/
fun contentPadding(
start: Dp = TextFieldPadding,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/TextFieldImpl.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/TextFieldImpl.kt
index adda9d9..34a9bfa 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/TextFieldImpl.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/TextFieldImpl.kt
@@ -67,6 +67,7 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
internal enum class TextFieldType {
Filled,
@@ -490,6 +491,8 @@
private const val PlaceholderAnimationDelayOrDuration = 67
internal val TextFieldPadding = 16.dp
+// SP not DP because it should scale with font size. Value equal to bodySmall line height / 2.
+internal val TextFieldLabelExtraPadding = 8.sp
internal val HorizontalIconPadding = 12.dp
internal val SupportingTopPadding = 4.dp
internal val PrefixSuffixTextPadding = 2.dp
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/NavigationDrawer.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/NavigationDrawer.desktop.kt
deleted file mode 100644
index 94baaa8..0000000
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/NavigationDrawer.desktop.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-
-/**
- * A predictive back handler that does nothing when running in a desktop context, since predictive
- * back is only supported on Android.
- *
- * @param drawerState state of the drawer
- * @param content content of the rest of the UI
- */
-@Composable
-internal actual fun DrawerPredictiveBackHandler(
- drawerState: DrawerState,
- content: @Composable (DrawerPredictiveBackState) -> Unit
-) {
- content(remember { DrawerPredictiveBackState() })
-}
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Tooltip.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Tooltip.desktop.kt
deleted file mode 100644
index 1085812..0000000
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Tooltip.desktop.kt
+++ /dev/null
@@ -1,168 +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.compose.material3
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.paddingFromBaseline
-import androidx.compose.foundation.layout.requiredHeightIn
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.material3.tokens.PlainTooltipTokens
-import androidx.compose.material3.tokens.RichTooltipTokens
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.DpSize
-
-/**
- * Plain tooltip that provides a descriptive message.
- *
- * Usually used with [TooltipBox].
- *
- * @param modifier the [Modifier] to be applied to the tooltip.
- * @param caretSize [DpSize] for the caret of the tooltip, if a default caret is desired with a
- * specific dimension. Please see [TooltipDefaults.caretSize] to see the default dimensions. Pass
- * in Dp.Unspecified for this parameter if no caret is desired.
- * @param shape the [Shape] that should be applied to the tooltip container.
- * @param contentColor [Color] that will be applied to the tooltip's content.
- * @param containerColor [Color] that will be applied to the tooltip's container.
- * @param tonalElevation the tonal elevation of the tooltip.
- * @param shadowElevation the shadow elevation of the tooltip.
- * @param content the composable that will be used to populate the tooltip's content.
- */
-@Composable
-@ExperimentalMaterial3Api
-actual fun TooltipScope.PlainTooltip(
- modifier: Modifier,
- caretSize: DpSize,
- shape: Shape,
- contentColor: Color,
- containerColor: Color,
- tonalElevation: Dp,
- shadowElevation: Dp,
- content: @Composable () -> Unit
-) {
- Surface(
- modifier = modifier,
- shape = shape,
- color = containerColor,
- tonalElevation = tonalElevation,
- shadowElevation = shadowElevation
- ) {
- Box(
- modifier =
- Modifier.sizeIn(
- minWidth = TooltipMinWidth,
- maxWidth = PlainTooltipMaxWidth,
- minHeight = TooltipMinHeight
- )
- .padding(PlainTooltipContentPadding)
- ) {
- val textStyle = PlainTooltipTokens.SupportingTextFont.value
- CompositionLocalProvider(
- LocalContentColor provides contentColor,
- LocalTextStyle provides textStyle,
- content = content
- )
- }
- }
-}
-
-/**
- * Rich text tooltip that allows the user to pass in a title, text, and action. Tooltips are used to
- * provide a descriptive message.
- *
- * Usually used with [TooltipBox]
- *
- * @param modifier the [Modifier] to be applied to the tooltip.
- * @param title An optional title for the tooltip.
- * @param action An optional action for the tooltip.
- * @param caretSize [DpSize] for the caret of the tooltip, if a default caret is desired with a
- * specific dimension. Please see [TooltipDefaults.caretSize] to see the default dimensions. Pass
- * in Dp.Unspecified for this parameter if no caret is desired.
- * @param shape the [Shape] that should be applied to the tooltip container.
- * @param colors [RichTooltipColors] that will be applied to the tooltip's container and content.
- * @param tonalElevation the tonal elevation of the tooltip.
- * @param shadowElevation the shadow elevation of the tooltip.
- * @param text the composable that will be used to populate the rich tooltip's text.
- */
-@Composable
-@ExperimentalMaterial3Api
-actual fun TooltipScope.RichTooltip(
- modifier: Modifier,
- title: (@Composable () -> Unit)?,
- action: (@Composable () -> Unit)?,
- caretSize: DpSize,
- shape: Shape,
- colors: RichTooltipColors,
- tonalElevation: Dp,
- shadowElevation: Dp,
- text: @Composable () -> Unit
-) {
- Surface(
- modifier =
- modifier.sizeIn(
- minWidth = TooltipMinWidth,
- maxWidth = RichTooltipMaxWidth,
- minHeight = TooltipMinHeight
- ),
- shape = shape,
- color = colors.containerColor,
- tonalElevation = tonalElevation,
- shadowElevation = shadowElevation
- ) {
- val actionLabelTextStyle = RichTooltipTokens.ActionLabelTextFont.value
- val subheadTextStyle = RichTooltipTokens.SubheadFont.value
- val supportingTextStyle = RichTooltipTokens.SupportingTextFont.value
-
- Column(modifier = Modifier.padding(horizontal = RichTooltipHorizontalPadding)) {
- title?.let {
- Box(modifier = Modifier.paddingFromBaseline(top = HeightToSubheadFirstLine)) {
- CompositionLocalProvider(
- LocalContentColor provides colors.titleContentColor,
- LocalTextStyle provides subheadTextStyle,
- content = it
- )
- }
- }
- Box(modifier = Modifier.textVerticalPadding(title != null, action != null)) {
- CompositionLocalProvider(
- LocalContentColor provides colors.contentColor,
- LocalTextStyle provides supportingTextStyle,
- content = text
- )
- }
- action?.let {
- Box(
- modifier =
- Modifier.requiredHeightIn(min = ActionLabelMinHeight)
- .padding(bottom = ActionLabelBottomPadding)
- ) {
- CompositionLocalProvider(
- LocalContentColor provides colors.actionContentColor,
- LocalTextStyle provides actionLabelTextStyle,
- content = it
- )
- }
- }
- }
- }
-}
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/BasicTooltip.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/BasicTooltip.desktop.kt
deleted file mode 100644
index a689144..0000000
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/BasicTooltip.desktop.kt
+++ /dev/null
@@ -1,77 +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.compose.material3.internal
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.TooltipState
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.window.Popup
-import androidx.compose.ui.window.PopupPositionProvider
-import androidx.compose.ui.window.PopupProperties
-
-/**
- * NOTICE: Fork from androidx.compose.foundation.BasicTooltip box since those are experimental
- *
- * Tooltip that provides a descriptive message for an anchor. It can be used to call the users
- * attention to the anchor.
- *
- * @param positionProvider [PopupPositionProvider] that will be used to place the tooltip relative
- * to the anchor content.
- * @param tooltip the composable that will be used to populate the tooltip's content.
- * @param state handles the state of the tooltip's visibility.
- * @param modifier the [Modifier] to be applied to this BasicTooltipBox.
- * @param focusable [Boolean] that determines if the tooltip is focusable. When true, the tooltip
- * will consume touch events while it's shown and will have accessibility focus move to the first
- * element of the component. When false, the tooltip won't consume touch events while it's shown
- * but assistive-tech users will need to swipe or drag to get to the first element of the
- * component.
- * @param enableUserInput [Boolean] which determines if this BasicTooltipBox will handle long press
- * and mouse hover to trigger the tooltip through the state provided.
- * @param content the composable that the tooltip will anchor to.
- */
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-internal actual fun BasicTooltipBox(
- positionProvider: PopupPositionProvider,
- tooltip: @Composable () -> Unit,
- state: TooltipState,
- modifier: Modifier,
- focusable: Boolean,
- enableUserInput: Boolean,
- content: @Composable () -> Unit
-) {
- // TODO: Reuse android implementation - there is no platform specifics here.
- // Use expect/actual only for string resources
- Box(modifier = modifier) {
- content()
- if (state.isVisible) {
- Popup(
- popupPositionProvider = positionProvider,
- onDismissRequest = { state.dismiss() },
- properties =
- PopupProperties(
- // TODO(b/326167778): focusable = true cannot work with mouse
- focusable = false
- )
- ) {
- tooltip()
- }
- }
- }
-}
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/CalendarModel.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/CalendarModel.desktop.kt
deleted file mode 100644
index 2e183af..0000000
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/CalendarModel.desktop.kt
+++ /dev/null
@@ -1,48 +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.compose.material3.internal
-
-import androidx.compose.material3.CalendarLocale
-
-/** Returns a [CalendarModel] to be used by the date picker. */
-internal actual fun createCalendarModel(locale: CalendarLocale): CalendarModel =
- LegacyCalendarModelImpl(locale)
-
-/**
- * Formats a UTC timestamp into a string with a given date format skeleton.
- *
- * @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
- * @param skeleton a date format skeleton
- * @param locale the [CalendarLocale] to use when formatting the given timestamp
- * @param cache a [MutableMap] for caching formatter related results for better performance
- */
-internal actual fun formatWithSkeleton(
- utcTimeMillis: Long,
- skeleton: String,
- locale: CalendarLocale,
- cache: MutableMap<String, Any>
-): String {
- // Note: there is no equivalent in Java for Android's DateFormat.getBestDateTimePattern.
- // The JDK SimpleDateFormat expects a pattern, so the results will be "2023Jan7",
- // "2023January", etc. in case a skeleton holds an actual ICU skeleton and not a pattern.
- return LegacyCalendarModelImpl.formatWithPattern(
- utcTimeMillis = utcTimeMillis,
- pattern = skeleton,
- locale = locale,
- cache = cache
- )
-}
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/Strings.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/Strings.desktop.kt
deleted file mode 100644
index 4e0f782..0000000
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/Strings.desktop.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3.internal
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.ReadOnlyComposable
-import java.util.Locale
-
-@Composable
-@ReadOnlyComposable
-internal actual fun getString(string: Strings): String {
- return when (string) {
- Strings.NavigationMenu -> "Navigation menu"
- Strings.CloseDrawer -> "Close navigation menu"
- Strings.CloseSheet -> "Close sheet"
- Strings.DefaultErrorMessage -> "Invalid input"
- Strings.SliderRangeStart -> "Range Start"
- Strings.SliderRangeEnd -> "Range End"
- Strings.Dialog -> "Dialog"
- Strings.MenuExpanded -> "Expanded"
- Strings.MenuCollapsed -> "Collapsed"
- Strings.ToggleDropdownMenu -> "Toggle dropdown menu"
- Strings.SnackbarDismiss -> "Dismiss"
- Strings.SearchBarSearch -> "Search"
- Strings.SuggestionsAvailable -> "Suggestions below"
- Strings.DatePickerTitle -> "Select date"
- Strings.DatePickerHeadline -> "Selected date"
- Strings.DatePickerYearPickerPaneTitle -> "Year picker visible"
- Strings.DatePickerSwitchToYearSelection -> "Switch to selecting a year"
- Strings.DatePickerSwitchToDaySelection ->
- "Swipe to select a year, or tap to switch " + "back to selecting a day"
- Strings.DatePickerSwitchToNextMonth -> "Change to next month"
- Strings.DatePickerSwitchToPreviousMonth -> "Change to previous month"
- Strings.DatePickerNavigateToYearDescription -> "Navigate to year %1$"
- Strings.DatePickerHeadlineDescription -> "Current selection: %1$"
- Strings.DatePickerNoSelectionDescription -> "None"
- Strings.DatePickerTodayDescription -> "Today"
- Strings.DatePickerScrollToShowLaterYears -> "Scroll to show later years"
- Strings.DatePickerScrollToShowEarlierYears -> "Scroll to show earlier years"
- Strings.DateInputTitle -> "Select date"
- Strings.DateInputHeadline -> "Entered date"
- Strings.DateInputLabel -> "Date"
- Strings.DateInputHeadlineDescription -> "Entered date: %1$"
- Strings.DateInputNoInputDescription -> "None"
- Strings.DateInputInvalidNotAllowed -> "Date not allowed: %1$"
- Strings.DateInputInvalidForPattern -> "Date does not match expected pattern: %1$"
- Strings.DateInputInvalidYearRange -> "Date out of expected year range %1$ - %2$"
- Strings.DatePickerSwitchToCalendarMode -> "Switch to calendar input mode"
- Strings.DatePickerSwitchToInputMode -> "Switch to text input mode"
- Strings.DateRangePickerTitle -> "Select dates"
- Strings.DateRangePickerStartHeadline -> "Start date"
- Strings.DateRangePickerEndHeadline -> "End date"
- Strings.DateRangePickerScrollToShowNextMonth -> "Scroll to show the next month"
- Strings.DateRangePickerScrollToShowPreviousMonth -> "Scroll to show the previous month"
- Strings.DateRangePickerDayInRange -> "In range"
- Strings.DateRangeInputTitle -> "Enter dates"
- Strings.DateRangeInputInvalidRangeInput -> "Invalid date range input"
- Strings.BottomSheetPaneTitle -> "Bottom Sheet"
- Strings.BottomSheetDragHandleDescription -> "Drag Handle"
- Strings.BottomSheetPartialExpandDescription -> "Collapse bottom sheet"
- Strings.BottomSheetDismissDescription -> "Dismiss bottom sheet"
- Strings.BottomSheetExpandDescription -> "Expand bottom sheet"
- Strings.TooltipLongPressLabel -> "Show tooltip"
- Strings.TimePickerAM -> "AM"
- Strings.TimePickerPM -> "PM"
- Strings.TimePickerPeriodToggle -> "Select AM or PM"
- Strings.TimePickerMinuteSelection -> "Select minutes"
- Strings.TimePickerHourSelection -> "Select hour"
- Strings.TimePickerHourSuffix -> "%1$ o\\'clock"
- Strings.TimePickerMinuteSuffix -> "%1$ minutes"
- Strings.TimePicker24HourSuffix -> "%1$ hours"
- Strings.TimePickerMinute -> "Minute"
- Strings.TimePickerHour -> "Hour"
- Strings.TimePickerMinuteTextField -> "for minutes"
- Strings.TimePickerHourTextField -> "for hour"
- Strings.TooltipPaneDescription -> "Tooltip"
- else -> ""
- }
-}
-
-@JvmInline
-@Immutable
-internal actual value class Strings constructor(val value: Int) {
- actual companion object {
- actual val NavigationMenu = Strings(0)
- actual val CloseDrawer = Strings(1)
- actual val CloseSheet = Strings(2)
- actual val DefaultErrorMessage = Strings(3)
- actual val SliderRangeStart = Strings(4)
- actual val SliderRangeEnd = Strings(5)
- actual val Dialog = Strings(6)
- actual val MenuExpanded = Strings(7)
- actual val MenuCollapsed = Strings(8)
- actual val SnackbarDismiss = Strings(9)
- actual val SearchBarSearch = Strings(10)
- actual val SuggestionsAvailable = Strings(11)
- actual val DatePickerTitle = Strings(12)
- actual val DatePickerHeadline = Strings(13)
- actual val DatePickerYearPickerPaneTitle = Strings(14)
- actual val DatePickerSwitchToYearSelection = Strings(15)
- actual val DatePickerSwitchToDaySelection = Strings(16)
- actual val DatePickerSwitchToNextMonth = Strings(17)
- actual val DatePickerSwitchToPreviousMonth = Strings(18)
- actual val DatePickerNavigateToYearDescription = Strings(19)
- actual val DatePickerHeadlineDescription = Strings(20)
- actual val DatePickerNoSelectionDescription = Strings(21)
- actual val DatePickerTodayDescription = Strings(22)
- actual val DatePickerScrollToShowLaterYears = Strings(23)
- actual val DatePickerScrollToShowEarlierYears = Strings(24)
- actual val DateInputTitle = Strings(25)
- actual val DateInputHeadline = Strings(26)
- actual val DateInputLabel = Strings(27)
- actual val DateInputHeadlineDescription = Strings(28)
- actual val DateInputNoInputDescription = Strings(29)
- actual val DateInputInvalidNotAllowed = Strings(30)
- actual val DateInputInvalidForPattern = Strings(31)
- actual val DateInputInvalidYearRange = Strings(32)
- actual val DatePickerSwitchToCalendarMode = Strings(33)
- actual val DatePickerSwitchToInputMode = Strings(34)
- actual val DateRangePickerTitle = Strings(35)
- actual val DateRangePickerStartHeadline = Strings(36)
- actual val DateRangePickerEndHeadline = Strings(37)
- actual val DateRangePickerScrollToShowNextMonth = Strings(38)
- actual val DateRangePickerScrollToShowPreviousMonth = Strings(39)
- actual val DateRangePickerDayInRange = Strings(40)
- actual val DateRangeInputTitle = Strings(41)
- actual val DateRangeInputInvalidRangeInput = Strings(42)
- actual val BottomSheetPaneTitle = Strings(43)
- actual val BottomSheetDragHandleDescription = Strings(44)
- actual val BottomSheetPartialExpandDescription = Strings(45)
- actual val BottomSheetDismissDescription = Strings(46)
- actual val BottomSheetExpandDescription = Strings(47)
- actual val TooltipLongPressLabel = Strings(48)
- actual val TimePickerAM = Strings(49)
- actual val TimePickerPM = Strings(50)
- actual val TimePickerPeriodToggle = Strings(51)
- actual val TimePickerHourSelection = Strings(52)
- actual val TimePickerMinuteSelection = Strings(53)
- actual val TimePickerHourSuffix = Strings(54)
- actual val TimePicker24HourSuffix = Strings(55)
- actual val TimePickerMinuteSuffix = Strings(56)
- actual val TimePickerHour = Strings(57)
- actual val TimePickerMinute = Strings(58)
- actual val TimePickerHourTextField = Strings(59)
- actual val TimePickerMinuteTextField = Strings(60)
- actual val TooltipPaneDescription = Strings(61)
- actual val ExposedDropdownMenu = Strings(62)
- actual val ToggleDropdownMenu = Strings(63)
- }
-}
-
-@Composable
-@ReadOnlyComposable
-internal actual fun getString(string: Strings, vararg formatArgs: Any): String =
- String.format(getString(string), Locale.getDefault(), *formatArgs)
diff --git a/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/AlertDialog.skiko.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/AlertDialog.jvmStubs.kt
similarity index 63%
rename from compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/AlertDialog.skiko.kt
rename to compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/AlertDialog.jvmStubs.kt
index da00d6f..05eb985 100644
--- a/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/AlertDialog.skiko.kt
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/AlertDialog.jvmStubs.kt
@@ -23,10 +23,6 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.window.DialogProperties
-// Keep expect/actual for maintain binary compatibility.
-// `@file:JvmName` doesn't work here because Android and Desktop were published with different names
-// Please note that binary compatibility for Desktop is tracked only in JetBrains fork
-
@Composable
actual fun AlertDialog(
onDismissRequest: () -> Unit,
@@ -43,20 +39,4 @@
textContentColor: Color,
tonalElevation: Dp,
properties: DialogProperties
-): Unit =
- AlertDialogImpl(
- onDismissRequest = onDismissRequest,
- confirmButton = confirmButton,
- modifier = modifier,
- dismissButton = dismissButton,
- icon = icon,
- title = title,
- text = text,
- shape = shape,
- containerColor = containerColor,
- iconContentColor = iconContentColor,
- titleContentColor = titleContentColor,
- textContentColor = textContentColor,
- tonalElevation = tonalElevation,
- properties = properties
- )
+): Unit = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/CalendarLocale.desktop.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/CalendarLocale.jvmStubs.kt
similarity index 90%
rename from compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/CalendarLocale.desktop.kt
rename to compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/CalendarLocale.jvmStubs.kt
index 3f24336..02ed679 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/CalendarLocale.desktop.kt
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/CalendarLocale.jvmStubs.kt
@@ -23,4 +23,4 @@
@Composable
@ReadOnlyComposable
@OptIn(ExperimentalMaterial3Api::class)
-internal actual fun defaultLocale(): CalendarLocale = java.util.Locale.getDefault()
+internal actual fun defaultLocale(): CalendarLocale = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/ModalBottomSheet.skiko.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/ModalBottomSheet.jvmStubs.kt
similarity index 67%
rename from compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/ModalBottomSheet.skiko.kt
rename to compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/ModalBottomSheet.jvmStubs.kt
index 15778bb..d1e0033 100644
--- a/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/ModalBottomSheet.skiko.kt
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/ModalBottomSheet.jvmStubs.kt
@@ -20,32 +20,18 @@
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
-import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
@Immutable
@ExperimentalMaterial3Api
actual class ModalBottomSheetProperties
actual constructor(
actual val shouldDismissOnBackPress: Boolean,
-) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is ModalBottomSheetProperties) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = shouldDismissOnBackPress.hashCode()
- return result
- }
-}
+)
@Immutable
@ExperimentalMaterial3Api
actual object ModalBottomSheetDefaults {
- actual val properties = ModalBottomSheetProperties()
+ actual val properties: ModalBottomSheetProperties = implementedInJetBrainsFork()
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -55,10 +41,4 @@
properties: ModalBottomSheetProperties,
predictiveBackProgress: Animatable<Float, AnimationVector1D>,
content: @Composable () -> Unit
-) {
- Dialog(
- onDismissRequest = onDismissRequest,
- properties = DialogProperties(dismissOnBackPress = properties.shouldDismissOnBackPress),
- content = content
- )
-}
+): Unit = implementedInJetBrainsFork()
diff --git a/compose/material3/adaptive/adaptive/src/desktopMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.desktop.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/NavigationDrawer.jvmStubs.kt
similarity index 75%
copy from compose/material3/adaptive/adaptive/src/desktopMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.desktop.kt
copy to compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/NavigationDrawer.jvmStubs.kt
index a32d0d4..39fe5a5 100644
--- a/compose/material3/adaptive/adaptive/src/desktopMain/kotlin/androidx/compose/material3/adaptive/DesktopWindowInfo.desktop.kt
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/NavigationDrawer.jvmStubs.kt
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive
+package androidx.compose.material3
import androidx.compose.runtime.Composable
@Composable
-actual fun currentWindowAdaptiveInfo(): WindowAdaptiveInfo {
- TODO("Not yet implemented")
-}
+internal actual fun DrawerPredictiveBackHandler(
+ drawerState: DrawerState,
+ content: @Composable (DrawerPredictiveBackState) -> Unit
+): Unit = implementedInJetBrainsFork()
diff --git a/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/NotImplemented.jvmStubs.kt
similarity index 64%
copy from compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
copy to compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/NotImplemented.jvmStubs.kt
index d4a754b..faf2f8f 100644
--- a/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/NotImplemented.jvmStubs.kt
@@ -14,8 +14,14 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive.layout
+package androidx.compose.material3
-import androidx.compose.ui.Modifier
-
-internal actual fun Modifier.systemGestureExclusion(): Modifier = this
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun implementedInJetBrainsFork(): Nothing =
+ throw NotImplementedError(
+ """
+ Implemented only in JetBrains fork.
+ Please use `org.jetbrains.compose.material3:material3` package instead.
+ """
+ .trimIndent()
+ )
diff --git a/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/SkikoMenu.jvmStubs.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/SkikoMenu.jvmStubs.kt
new file mode 100644
index 0000000..487bc51
--- /dev/null
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/SkikoMenu.jvmStubs.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.window.PopupProperties
+
+@Composable
+actual fun DropdownMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier,
+ offset: DpOffset,
+ scrollState: ScrollState,
+ properties: PopupProperties,
+ shape: Shape,
+ containerColor: Color,
+ tonalElevation: Dp,
+ shadowElevation: Dp,
+ border: BorderStroke?,
+ content: @Composable ColumnScope.() -> Unit
+): Unit = implementedInJetBrainsFork()
+
+@Composable
+actual fun DropdownMenuItem(
+ text: @Composable () -> Unit,
+ onClick: () -> Unit,
+ modifier: Modifier,
+ leadingIcon: @Composable (() -> Unit)?,
+ trailingIcon: @Composable (() -> Unit)?,
+ enabled: Boolean,
+ colors: MenuItemColors,
+ contentPadding: PaddingValues,
+ interactionSource: MutableInteractionSource?,
+): Unit = implementedInJetBrainsFork()
+
+internal actual val DefaultMenuProperties: PopupProperties = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/TimeFormat.desktop.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/TimeFormat.jvmStubs.kt
similarity index 90%
rename from compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/TimeFormat.desktop.kt
rename to compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/TimeFormat.jvmStubs.kt
index ca9055a..c07f8c7 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/TimeFormat.desktop.kt
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/TimeFormat.jvmStubs.kt
@@ -20,4 +20,4 @@
import androidx.compose.runtime.ReadOnlyComposable
internal actual val is24HourFormat: Boolean
- @Composable @ReadOnlyComposable get() = false
+ @Composable @ReadOnlyComposable get() = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/TimePicker.desktop.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/TimePicker.jvmStubs.kt
similarity index 94%
rename from compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/TimePicker.desktop.kt
rename to compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/TimePicker.jvmStubs.kt
index 530eca7..8b28f7b 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/TimePicker.desktop.kt
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/TimePicker.jvmStubs.kt
@@ -17,5 +17,4 @@
package androidx.compose.material3
@OptIn(ExperimentalMaterial3Api::class)
-internal actual val defaultTimePickerLayoutType: TimePickerLayoutType =
- TimePickerLayoutType.Vertical
+internal actual val defaultTimePickerLayoutType: TimePickerLayoutType = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/Tooltip.jvmStubs.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/Tooltip.jvmStubs.kt
new file mode 100644
index 0000000..c4692a6
--- /dev/null
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/Tooltip.jvmStubs.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
+
+@Composable
+@ExperimentalMaterial3Api
+actual fun TooltipScope.PlainTooltip(
+ modifier: Modifier,
+ caretSize: DpSize,
+ shape: Shape,
+ contentColor: Color,
+ containerColor: Color,
+ tonalElevation: Dp,
+ shadowElevation: Dp,
+ content: @Composable () -> Unit
+): Unit = implementedInJetBrainsFork()
+
+@Composable
+@ExperimentalMaterial3Api
+actual fun TooltipScope.RichTooltip(
+ modifier: Modifier,
+ title: (@Composable () -> Unit)?,
+ action: (@Composable () -> Unit)?,
+ caretSize: DpSize,
+ shape: Shape,
+ colors: RichTooltipColors,
+ tonalElevation: Dp,
+ shadowElevation: Dp,
+ text: @Composable () -> Unit
+): Unit = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.desktop.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.jvmStubs.kt
similarity index 84%
rename from compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.desktop.kt
rename to compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.jvmStubs.kt
index d4e155e..e68e24c 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.desktop.kt
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/AccessibilityServiceStateProvider.jvmStubs.kt
@@ -16,15 +16,12 @@
package androidx.compose.material3.internal
+import androidx.compose.material3.implementedInJetBrainsFork
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
@Composable
internal actual fun rememberAccessibilityServiceState(
listenToTouchExplorationState: Boolean,
listenToSwitchAccessState: Boolean,
-): State<Boolean> {
- return remember { mutableStateOf(false) }
-}
+): State<Boolean> = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/BasicTooltip.jvmStubs.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/BasicTooltip.jvmStubs.kt
new file mode 100644
index 0000000..c72051a
--- /dev/null
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/BasicTooltip.jvmStubs.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.internal
+
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.TooltipState
+import androidx.compose.material3.implementedInJetBrainsFork
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.window.PopupPositionProvider
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal actual fun BasicTooltipBox(
+ positionProvider: PopupPositionProvider,
+ tooltip: @Composable () -> Unit,
+ state: TooltipState,
+ modifier: Modifier,
+ focusable: Boolean,
+ enableUserInput: Boolean,
+ content: @Composable () -> Unit
+): Unit = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.desktop.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/CalendarModel.jvmStubs.kt
similarity index 61%
copy from compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.desktop.kt
copy to compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/CalendarModel.jvmStubs.kt
index fef57d6..ab14c60 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.desktop.kt
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/CalendarModel.jvmStubs.kt
@@ -16,9 +16,15 @@
package androidx.compose.material3.internal
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.unit.dp
+import androidx.compose.material3.CalendarLocale
+import androidx.compose.material3.implementedInJetBrainsFork
-internal actual val WindowInsets.Companion.systemBarsForVisualComponents: WindowInsets
- @Composable get() = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)
+internal actual fun createCalendarModel(locale: CalendarLocale): CalendarModel =
+ implementedInJetBrainsFork()
+
+internal actual fun formatWithSkeleton(
+ utcTimeMillis: Long,
+ skeleton: String,
+ locale: CalendarLocale,
+ cache: MutableMap<String, Any>
+): String = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.desktop.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.jvmStubs.kt
similarity index 88%
rename from compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.desktop.kt
rename to compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.jvmStubs.kt
index 7bf9d47..2a27f48 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.desktop.kt
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/DefaultPlatformTextStyle.jvmStubs.kt
@@ -16,6 +16,7 @@
package androidx.compose.material3.internal
+import androidx.compose.material3.implementedInJetBrainsFork
import androidx.compose.ui.text.PlatformTextStyle
-internal actual fun defaultPlatformTextStyle(): PlatformTextStyle? = null
+internal actual fun defaultPlatformTextStyle(): PlatformTextStyle? = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/Strings.jvmStubs.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/Strings.jvmStubs.kt
new file mode 100644
index 0000000..8b30e91
--- /dev/null
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/Strings.jvmStubs.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.internal
+
+import androidx.compose.material3.implementedInJetBrainsFork
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
+
+@Composable
+@ReadOnlyComposable
+internal actual fun getString(string: Strings): String = implementedInJetBrainsFork()
+
+@JvmInline
+@Immutable
+internal actual value class Strings constructor(val value: Int) {
+ actual companion object {
+ actual val NavigationMenu: Strings = implementedInJetBrainsFork()
+ actual val CloseDrawer: Strings = implementedInJetBrainsFork()
+ actual val CloseSheet: Strings = implementedInJetBrainsFork()
+ actual val DefaultErrorMessage: Strings = implementedInJetBrainsFork()
+ actual val SliderRangeStart: Strings = implementedInJetBrainsFork()
+ actual val SliderRangeEnd: Strings = implementedInJetBrainsFork()
+ actual val Dialog: Strings = implementedInJetBrainsFork()
+ actual val MenuExpanded: Strings = implementedInJetBrainsFork()
+ actual val MenuCollapsed: Strings = implementedInJetBrainsFork()
+ actual val SnackbarDismiss: Strings = implementedInJetBrainsFork()
+ actual val SearchBarSearch: Strings = implementedInJetBrainsFork()
+ actual val SuggestionsAvailable: Strings = implementedInJetBrainsFork()
+ actual val DatePickerTitle: Strings = implementedInJetBrainsFork()
+ actual val DatePickerHeadline: Strings = implementedInJetBrainsFork()
+ actual val DatePickerYearPickerPaneTitle: Strings = implementedInJetBrainsFork()
+ actual val DatePickerSwitchToYearSelection: Strings = implementedInJetBrainsFork()
+ actual val DatePickerSwitchToDaySelection: Strings = implementedInJetBrainsFork()
+ actual val DatePickerSwitchToNextMonth: Strings = implementedInJetBrainsFork()
+ actual val DatePickerSwitchToPreviousMonth: Strings = implementedInJetBrainsFork()
+ actual val DatePickerNavigateToYearDescription: Strings = implementedInJetBrainsFork()
+ actual val DatePickerHeadlineDescription: Strings = implementedInJetBrainsFork()
+ actual val DatePickerNoSelectionDescription: Strings = implementedInJetBrainsFork()
+ actual val DatePickerTodayDescription: Strings = implementedInJetBrainsFork()
+ actual val DatePickerScrollToShowLaterYears: Strings = implementedInJetBrainsFork()
+ actual val DatePickerScrollToShowEarlierYears: Strings = implementedInJetBrainsFork()
+ actual val DateInputTitle: Strings = implementedInJetBrainsFork()
+ actual val DateInputHeadline: Strings = implementedInJetBrainsFork()
+ actual val DateInputLabel: Strings = implementedInJetBrainsFork()
+ actual val DateInputHeadlineDescription: Strings = implementedInJetBrainsFork()
+ actual val DateInputNoInputDescription: Strings = implementedInJetBrainsFork()
+ actual val DateInputInvalidNotAllowed: Strings = implementedInJetBrainsFork()
+ actual val DateInputInvalidForPattern: Strings = implementedInJetBrainsFork()
+ actual val DateInputInvalidYearRange: Strings = implementedInJetBrainsFork()
+ actual val DatePickerSwitchToCalendarMode: Strings = implementedInJetBrainsFork()
+ actual val DatePickerSwitchToInputMode: Strings = implementedInJetBrainsFork()
+ actual val DateRangePickerTitle: Strings = implementedInJetBrainsFork()
+ actual val DateRangePickerStartHeadline: Strings = implementedInJetBrainsFork()
+ actual val DateRangePickerEndHeadline: Strings = implementedInJetBrainsFork()
+ actual val DateRangePickerScrollToShowNextMonth: Strings = implementedInJetBrainsFork()
+ actual val DateRangePickerScrollToShowPreviousMonth: Strings = implementedInJetBrainsFork()
+ actual val DateRangePickerDayInRange: Strings = implementedInJetBrainsFork()
+ actual val DateRangeInputTitle: Strings = implementedInJetBrainsFork()
+ actual val DateRangeInputInvalidRangeInput: Strings = implementedInJetBrainsFork()
+ actual val BottomSheetPaneTitle: Strings = implementedInJetBrainsFork()
+ actual val BottomSheetDragHandleDescription: Strings = implementedInJetBrainsFork()
+ actual val BottomSheetPartialExpandDescription: Strings = implementedInJetBrainsFork()
+ actual val BottomSheetDismissDescription: Strings = implementedInJetBrainsFork()
+ actual val BottomSheetExpandDescription: Strings = implementedInJetBrainsFork()
+ actual val TooltipLongPressLabel: Strings = implementedInJetBrainsFork()
+ actual val TimePickerAM: Strings = implementedInJetBrainsFork()
+ actual val TimePickerPM: Strings = implementedInJetBrainsFork()
+ actual val TimePickerPeriodToggle: Strings = implementedInJetBrainsFork()
+ actual val TimePickerHourSelection: Strings = implementedInJetBrainsFork()
+ actual val TimePickerMinuteSelection: Strings = implementedInJetBrainsFork()
+ actual val TimePickerHourSuffix: Strings = implementedInJetBrainsFork()
+ actual val TimePicker24HourSuffix: Strings = implementedInJetBrainsFork()
+ actual val TimePickerMinuteSuffix: Strings = implementedInJetBrainsFork()
+ actual val TimePickerHour: Strings = implementedInJetBrainsFork()
+ actual val TimePickerMinute: Strings = implementedInJetBrainsFork()
+ actual val TimePickerHourTextField: Strings = implementedInJetBrainsFork()
+ actual val TimePickerMinuteTextField: Strings = implementedInJetBrainsFork()
+ actual val TooltipPaneDescription: Strings = implementedInJetBrainsFork()
+ actual val ExposedDropdownMenu: Strings = implementedInJetBrainsFork()
+ actual val ToggleDropdownMenu: Strings = implementedInJetBrainsFork()
+ }
+}
+
+@Composable
+@ReadOnlyComposable
+internal actual fun getString(string: Strings, vararg formatArgs: Any): String =
+ implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.desktop.kt b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.jvmStubs.kt
similarity index 87%
rename from compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.desktop.kt
rename to compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.jvmStubs.kt
index fef57d6..5c6bf7d 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.desktop.kt
+++ b/compose/material3/material3/src/jvmStubsMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.jvmStubs.kt
@@ -17,8 +17,8 @@
package androidx.compose.material3.internal
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.material3.implementedInJetBrainsFork
import androidx.compose.runtime.Composable
-import androidx.compose.ui.unit.dp
internal actual val WindowInsets.Companion.systemBarsForVisualComponents: WindowInsets
- @Composable get() = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)
+ @Composable get() = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/SkikoMenu.skiko.kt b/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/SkikoMenu.skiko.kt
deleted file mode 100644
index d0d080b..0000000
--- a/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/SkikoMenu.skiko.kt
+++ /dev/null
@@ -1,291 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3
-
-import androidx.compose.animation.core.MutableTransitionState
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.ScrollState
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.material3.internal.DropdownMenuPositionProvider
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.input.InputMode
-import androidx.compose.ui.input.InputModeManager
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.KeyEventType
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.type
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.platform.LocalInputModeManager
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.DpOffset
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Popup
-import androidx.compose.ui.window.PopupProperties
-
-@Deprecated(
- "Replaced by DropdownMenu with properties parameter",
- ReplaceWith(
- "DropdownMenu(expanded, onDismissRequest, modifier, offset, " +
- "androidx.compose.ui.window.PopupProperties(focusable = focusable), " +
- "content)"
- ),
- level = DeprecationLevel.HIDDEN
-)
-@Suppress("ModifierParameter")
-@Composable
-fun DropdownMenu(
- expanded: Boolean,
- onDismissRequest: () -> Unit,
- focusable: Boolean = true,
- modifier: Modifier = Modifier,
- offset: DpOffset = DpOffset(0.dp, 0.dp),
- content: @Composable ColumnScope.() -> Unit
-) =
- DropdownMenu(
- expanded = expanded,
- onDismissRequest = onDismissRequest,
- modifier = modifier,
- offset = offset,
- properties = PopupProperties(focusable = focusable),
- content = content
- )
-
-// Workaround for `Overload resolution ambiguity` between old and new overload.
-@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
-@Composable
-fun DropdownMenu(
- expanded: Boolean,
- onDismissRequest: () -> Unit,
- modifier: Modifier = Modifier,
- offset: DpOffset = DpOffset(0.dp, 0.dp),
- content: @Composable ColumnScope.() -> Unit
-) =
- DropdownMenu(
- expanded = expanded,
- onDismissRequest = onDismissRequest,
- modifier = modifier,
- offset = offset,
- properties = PopupProperties(focusable = true),
- content = content
- )
-
-@Composable
-actual fun DropdownMenu(
- expanded: Boolean,
- onDismissRequest: () -> Unit,
- modifier: Modifier,
- offset: DpOffset,
- scrollState: ScrollState,
- properties: PopupProperties,
- shape: Shape,
- containerColor: Color,
- tonalElevation: Dp,
- shadowElevation: Dp,
- border: BorderStroke?,
- content: @Composable ColumnScope.() -> Unit
-) {
- val expandedState = remember { MutableTransitionState(false) }
- expandedState.targetState = expanded
-
- if (expandedState.currentState || expandedState.targetState) {
- val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
- val density = LocalDensity.current
- val popupPositionProvider =
- remember(offset, density) {
- DropdownMenuPositionProvider(offset, density) { parentBounds, menuBounds ->
- transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds)
- }
- }
-
- var focusManager: FocusManager? by mutableStateOf(null)
- var inputModeManager: InputModeManager? by mutableStateOf(null)
- Popup(
- onDismissRequest = onDismissRequest,
- popupPositionProvider = popupPositionProvider,
- properties = properties,
- onKeyEvent = { handlePopupOnKeyEvent(it, focusManager, inputModeManager) },
- ) {
- focusManager = LocalFocusManager.current
- inputModeManager = LocalInputModeManager.current
-
- DropdownMenuContent(
- expandedState = expandedState,
- transformOriginState = transformOriginState,
- scrollState = scrollState,
- shape = shape,
- containerColor = containerColor,
- tonalElevation = tonalElevation,
- shadowElevation = shadowElevation,
- border = border,
- modifier = modifier,
- content = content,
- )
- }
- }
-}
-
-@Deprecated(
- level = DeprecationLevel.HIDDEN,
- replaceWith =
- ReplaceWith(
- expression =
- "DropdownMenu(\n" +
- " expanded = expanded,\n" +
- " onDismissRequest = onDismissRequest,\n" +
- " modifier = modifier,\n" +
- " offset = offset,\n" +
- " scrollState = scrollState,\n" +
- " properties = properties,\n" +
- " shape = MenuDefaults.shape,\n" +
- " containerColor = MenuDefaults.containerColor,\n" +
- " tonalElevation = MenuDefaults.TonalElevation,\n" +
- " shadowElevation = MenuDefaults.ShadowElevation,\n" +
- " border = null,\n" +
- " content = content,\n" +
- ")",
- ),
- message =
- "Maintained for binary compatibility. Use overload with parameters for shape, " +
- "color, elevation, and border."
-)
-@Composable
-fun DropdownMenu(
- expanded: Boolean,
- onDismissRequest: () -> Unit,
- modifier: Modifier = Modifier,
- offset: DpOffset = DpOffset(0.dp, 0.dp),
- scrollState: ScrollState = rememberScrollState(),
- properties: PopupProperties = PopupProperties(focusable = true),
- content: @Composable ColumnScope.() -> Unit
-) =
- DropdownMenu(
- expanded = expanded,
- onDismissRequest = onDismissRequest,
- modifier = modifier,
- offset = offset,
- scrollState = scrollState,
- properties = properties,
- shape = MenuDefaults.shape,
- containerColor = MenuDefaults.containerColor,
- tonalElevation = MenuDefaults.TonalElevation,
- shadowElevation = MenuDefaults.ShadowElevation,
- border = null,
- content = content,
- )
-
-@Suppress("ModifierParameter")
-@OptIn(ExperimentalMaterial3Api::class)
-@Deprecated(
- level = DeprecationLevel.HIDDEN,
- replaceWith =
- ReplaceWith(
- expression =
- "DropdownMenu(expanded,onDismissRequest, modifier, offset, " +
- "rememberScrollState(), properties, content)",
- "androidx.compose.foundation.rememberScrollState"
- ),
- message = "Replaced by a DropdownMenu function with a ScrollState parameter"
-)
-@Composable
-fun DropdownMenu(
- expanded: Boolean,
- onDismissRequest: () -> Unit,
- modifier: Modifier,
- offset: DpOffset,
- properties: PopupProperties,
- content: @Composable ColumnScope.() -> Unit
-) =
- DropdownMenu(
- expanded = expanded,
- onDismissRequest = onDismissRequest,
- modifier = modifier,
- offset = offset,
- scrollState = rememberScrollState(),
- properties = properties,
- content = content
- )
-
-@Composable
-actual fun DropdownMenuItem(
- text: @Composable () -> Unit,
- onClick: () -> Unit,
- modifier: Modifier,
- leadingIcon: @Composable (() -> Unit)?,
- trailingIcon: @Composable (() -> Unit)?,
- enabled: Boolean,
- colors: MenuItemColors,
- contentPadding: PaddingValues,
- interactionSource: MutableInteractionSource?,
-) {
- DropdownMenuItemContent(
- text = text,
- onClick = onClick,
- modifier = modifier,
- leadingIcon = leadingIcon,
- trailingIcon = trailingIcon,
- enabled = enabled,
- colors = colors,
- contentPadding = contentPadding,
- interactionSource = interactionSource,
- )
-}
-
-internal actual val DefaultMenuProperties =
- PopupProperties(
- focusable = true
- // TODO: Add a flag to not block clicks outside while being focusable
- )
-
-@OptIn(ExperimentalComposeUiApi::class)
-private fun handlePopupOnKeyEvent(
- keyEvent: KeyEvent,
- focusManager: FocusManager?,
- inputModeManager: InputModeManager?
-): Boolean =
- if (keyEvent.type == KeyEventType.KeyDown) {
- when (keyEvent.key) {
- Key.DirectionDown -> {
- inputModeManager?.requestInputMode(InputMode.Keyboard)
- focusManager?.moveFocus(FocusDirection.Next)
- true
- }
- Key.DirectionUp -> {
- inputModeManager?.requestInputMode(InputMode.Keyboard)
- focusManager?.moveFocus(FocusDirection.Previous)
- true
- }
- else -> false
- }
- } else {
- false
- }
diff --git a/compose/runtime/OWNERS b/compose/runtime/OWNERS
index 1d23419..8db12da 100644
--- a/compose/runtime/OWNERS
+++ b/compose/runtime/OWNERS
@@ -1,5 +1,4 @@
# Bug component: 343210
[email protected]
[email protected]
[email protected]
[email protected]
diff --git a/compose/runtime/runtime-livedata/build.gradle b/compose/runtime/runtime-livedata/build.gradle
index 2655560..3efb105 100644
--- a/compose/runtime/runtime-livedata/build.gradle
+++ b/compose/runtime/runtime-livedata/build.gradle
@@ -37,7 +37,7 @@
api(project(":compose:runtime:runtime"))
api("androidx.lifecycle:lifecycle-livedata:2.6.1")
api("androidx.lifecycle:lifecycle-runtime:2.6.1")
- api("androidx.lifecycle:lifecycle-runtime-compose:2.8.2")
+ api("androidx.lifecycle:lifecycle-runtime-compose:2.8.3")
androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:test-utils"))
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index fce8c80..ee38e7d 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -184,7 +184,7 @@
public static final class Composer.Companion {
method public Object getEmpty();
- method @SuppressCompatibility @androidx.compose.runtime.InternalComposeTracingApi public void setTracer(androidx.compose.runtime.CompositionTracer tracer);
+ method @SuppressCompatibility @androidx.compose.runtime.InternalComposeTracingApi public void setTracer(androidx.compose.runtime.CompositionTracer? tracer);
property public final Object Empty;
}
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 3167f5a..00784cf 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -189,7 +189,7 @@
public static final class Composer.Companion {
method public Object getEmpty();
- method @SuppressCompatibility @androidx.compose.runtime.InternalComposeTracingApi public void setTracer(androidx.compose.runtime.CompositionTracer tracer);
+ method @SuppressCompatibility @androidx.compose.runtime.InternalComposeTracingApi public void setTracer(androidx.compose.runtime.CompositionTracer? tracer);
property public final Object Empty;
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index 41b22a3f..96121f1 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -1128,7 +1128,7 @@
* recompositions.
*/
@InternalComposeTracingApi
- fun setTracer(tracer: CompositionTracer) {
+ fun setTracer(tracer: CompositionTracer?) {
compositionTracer = tracer
}
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Stack.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Stack.kt
index 42f0d1a..81d7a7b7 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Stack.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Stack.kt
@@ -16,10 +16,8 @@
package androidx.compose.runtime
-// TODO(b/139762913): Consider changing to inline class of ArrayList<T> to avoid wrapper class
-internal class Stack<T> {
- private val backing = ArrayList<T>()
-
+@JvmInline
+internal value class Stack<T>(private val backing: ArrayList<T> = ArrayList()) {
val size: Int
get() = backing.size
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index f5d9bbf..9cb3aaf 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -809,6 +809,7 @@
val result =
innerApplyLocked(
nextSnapshotId,
+ modified,
optimisticMerges,
openSnapshots.clear(previousGlobalSnapshot.id)
)
@@ -957,6 +958,7 @@
internal fun innerApplyLocked(
snapshotId: Int,
+ modified: MutableScatterSet<StateObject>,
optimisticMerges: Map<StateRecord, StateRecord>?,
invalidSnapshots: SnapshotIdSet
): SnapshotApplyResult {
@@ -973,7 +975,6 @@
// is for the apply.
var mergedRecords: MutableList<Pair<StateObject, StateRecord>>? = null
val start = this.invalid.set(id).or(this.previousIds)
- val modified = modified!!
var statesToRemove: MutableList<StateObject>? = null
modified.forEach { state ->
val first = state.firstStateRecord
@@ -1481,7 +1482,7 @@
if (modified == null || modified.size == 0) {
closeAndReleasePinning()
} else {
- val result = innerApplyLocked(parent.id, optimisticMerges, parent.invalid)
+ val result = innerApplyLocked(parent.id, modified, optimisticMerges, parent.invalid)
if (result != SnapshotApplyResult.Success) return result
parent.modified?.apply { addAll(modified) }
diff --git a/compose/ui/ui-android-stubs/build.gradle b/compose/ui/ui-android-stubs/build.gradle
index 168677e..7b4498e 100644
--- a/compose/ui/ui-android-stubs/build.gradle
+++ b/compose/ui/ui-android-stubs/build.gradle
@@ -38,6 +38,7 @@
inceptionYear = "2020"
description = "Stubs for classes in older Android APIs"
metalavaK2UastEnabled = true
+ doNotDocumentReason = "Not published to maven"
}
android {
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
index 5081429..2b19a4f 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
@@ -73,7 +73,7 @@
// to increment the refcount of any outstanding layers again the next time the
// content is drawn
if (!predrawListenerRegistered) {
- layerManager.destroy()
+ layerManager.closeImageReaders()
ownerView.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
@@ -106,13 +106,13 @@
// When the View is detached from the window, remove the component callbacks
// used to listen to trim memory signals
unregisterComponentCallback(v.context)
- layerManager.destroy()
+ layerManager.closeImageReaders()
}
}
)
}
- fun isLayerManagerInitialized(): Boolean = layerManager.hasImageReader()
+ fun isLayerManagerInitialized(): Boolean = layerManager.hasImageReaders()
private fun registerComponentCallback(context: Context) {
if (!componentCallbackRegistered) {
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt
index 8208e46..b92926e 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt
@@ -50,7 +50,10 @@
@Suppress("NotCloseable")
actual class GraphicsLayer
-internal constructor(internal val impl: GraphicsLayerImpl, private val layerManager: LayerManager) {
+internal constructor(
+ internal val impl: GraphicsLayerImpl,
+ internal val layerManager: LayerManager
+) {
private var density = DefaultDensity
private var layoutDirection = LayoutDirection.Ltr
private var drawBlock: DrawScope.() -> Unit = {}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt
index 5b59be4..26cb74a 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt
@@ -20,13 +20,11 @@
import android.media.ImageReader
import android.os.Build
import android.os.Looper
-import android.os.Message
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.collection.MutableObjectList
-import androidx.collection.ScatterSet
import androidx.collection.mutableObjectListOf
-import androidx.collection.mutableScatterSetOf
+import androidx.collection.mutableScatterMapOf
import androidx.compose.ui.graphics.CanvasHolder
import androidx.core.os.HandlerCompat
@@ -37,54 +35,20 @@
*/
internal class LayerManager(val canvasHolder: CanvasHolder) {
- private val layerSet = mutableScatterSetOf<GraphicsLayer>()
-
/**
* Create a placeholder ImageReader instance that we will use to issue a single draw call for
* each GraphicsLayer. This placeholder draw will increase the ref count of each RenderNode
* instance within HWUI therefore persisting it across frames as there is another internal
* CanvasContext instance owned by the internal HwuiContext instance of a Surface
*/
- private var imageReader: ImageReader? = null
+ private val layerMap = mutableScatterMapOf<GraphicsLayer, ImageReader?>()
- private val handler =
- HandlerCompat.createAsync(Looper.getMainLooper()) {
- persistLayers(layerSet)
- true
- }
+ private val handler = HandlerCompat.createAsync(Looper.getMainLooper())
private var postponedReleaseRequests: MutableObjectList<GraphicsLayer>? = null
private var persistenceIterationInProgress = false
fun persist(layer: GraphicsLayer) {
- layerSet.add(layer)
- if (!handler.hasMessages(0)) {
- // we don't run persistLayers() synchronously in order to do less work as there
- // might be a lot of new layers created during one frame. however we also want
- // to execute it as soon as possible to be able to persist the layers before
- // they discard their content. it is possible that there is some other work
- // scheduled on the main thread which is going to change what layers are drawn.
- // we use sendMessageAtFrontOfQueue() in order to be executed before that.
- handler.sendMessageAtFrontOfQueue(Message.obtain())
- }
- }
-
- fun release(layer: GraphicsLayer) {
- if (!persistenceIterationInProgress) {
- if (layerSet.remove(layer)) {
- layer.discardDisplayList()
- }
- } else {
- // we can't remove an item from a list, which is currently being iterated.
- // so we use a second list to remember such requests
- val requests =
- postponedReleaseRequests
- ?: mutableObjectListOf<GraphicsLayer>().also { postponedReleaseRequests = it }
- requests.add(layer)
- }
- }
-
- private fun persistLayers(layers: ScatterSet<GraphicsLayer>) {
/**
* Create a placeholder ImageReader instance that we will use to issue a single draw call
* for each GraphicsLayer. This placeholder draw will increase the ref count of each
@@ -98,10 +62,10 @@
// surfaces as not being released even though the owning ImageReader does release the
// surface in ImageReader#close
// See b/340578758
- val shouldPersistLayers = requiredOsVersion && layers.isNotEmpty() && !isRobolectric
+ val shouldPersistLayers = requiredOsVersion && !isRobolectric
if (shouldPersistLayers) {
val reader =
- imageReader
+ layerMap[layer]
// 3 buffers is the default max buffers amount for a swapchain. The buffers are
// lazily allocated only if one is not available when it is requested.
?: ImageReader.newInstance(1, 1, PixelFormat.RGBA_8888, 3)
@@ -115,33 +79,48 @@
handler
)
}
- .also { imageReader = it }
+ .also { layerMap[layer] = it }
val surface = reader.surface
val canvas = LockHardwareCanvasHelper.lockHardwareCanvas(surface)
- persistenceIterationInProgress = true
canvasHolder.drawInto(canvas) {
canvas.save()
canvas.clipRect(0, 0, 1, 1)
- layers.forEach { layer -> layer.drawForPersistence(this) }
+ layer.drawForPersistence(this)
canvas.restore()
}
- persistenceIterationInProgress = false
- val requests = postponedReleaseRequests
- if (requests != null && requests.isNotEmpty()) {
- requests.forEach { release(it) }
- requests.clear()
- }
surface.unlockCanvasAndPost(canvas)
+ } else {
+ layerMap[layer] = null
}
}
- fun destroy() {
- imageReader?.close()
- imageReader = null
+ fun release(layer: GraphicsLayer) {
+ if (!persistenceIterationInProgress) {
+ if (layer.layerManager === this) {
+ layer.discardDisplayList()
+ layerMap.remove(layer)?.close()
+ }
+ } else {
+ // we can't remove an item from a list, which is currently being iterated.
+ // so we use a second list to remember such requests
+ val requests =
+ postponedReleaseRequests
+ ?: mutableObjectListOf<GraphicsLayer>().also { postponedReleaseRequests = it }
+ requests.add(layer)
+ }
}
- fun hasImageReader(): Boolean = imageReader != null
+ fun closeImageReaders() {
+ layerMap.forEach { layer, reader ->
+ reader?.apply {
+ close()
+ layerMap[layer] = null
+ }
+ }
+ }
+
+ fun hasImageReaders(): Boolean = layerMap.any { _, reader -> reader != null }
/**
* Discards the corresponding ImageReader used to increment the ref count of each layer and
@@ -150,8 +129,18 @@
* backgrounded
*/
fun updateLayerPersistence() {
- destroy()
- persistLayers(layerSet)
+ closeImageReaders()
+ persistenceIterationInProgress = true
+ try {
+ layerMap.forEachKey { layer -> persist(layer) }
+ } finally {
+ persistenceIterationInProgress = false
+ }
+ val requests = postponedReleaseRequests
+ if (requests != null && requests.isNotEmpty()) {
+ requests.forEach { release(it) }
+ requests.clear()
+ }
}
companion object {
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetector.kt
index 1168ecf..cfdc8a0 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetector.kt
@@ -28,27 +28,36 @@
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.android.tools.lint.detector.api.isIncorrectImplicitReturnInLambda
import com.intellij.psi.PsiMethod
import java.util.EnumSet
+import org.jetbrains.kotlin.analysis.api.analyze
+import org.jetbrains.kotlin.analysis.api.types.KtFunctionalType
+import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.ULocalVariable
import org.jetbrains.uast.UReturnExpression
+import org.jetbrains.uast.UVariable
import org.jetbrains.uast.skipParenthesizedExprUp
class ReturnFromAwaitPointerEventScopeDetector : Detector(), SourceCodeScanner {
-
override fun getApplicableMethodNames(): List<String> =
listOf(Names.Ui.Pointer.AwaitPointerEventScope.shortName)
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
if (!method.isInPackageName(Names.Ui.Pointer.PackageName)) return
+
val methodParent = skipParenthesizedExprUp(node.uastParent)
val isAssignedToVariable = methodParent is ULocalVariable
- val isReturnExpression =
- methodParent is UReturnExpression && !methodParent.isIncorrectImplicitReturnInLambda()
- if (isAssignedToVariable || isReturnExpression) {
+ val isReturnExpression = methodParent is UReturnExpression
+
+ val invalidUseOfAwaitPointerEventScopeWithReturn =
+ isReturnExpression && !validUseOfAwaitPointerEventScopeWithReturn(node)
+
+ if (isAssignedToVariable || invalidUseOfAwaitPointerEventScopeWithReturn) {
context.report(
ExitAwaitPointerEventScope,
node,
@@ -58,7 +67,63 @@
}
}
+ private fun validUseOfAwaitPointerEventScopeWithReturn(
+ awaitPointerEventScopeNode: UCallExpression
+ ): Boolean {
+ // Traverse up the UAST tree
+ var currentNode: UElement? = awaitPointerEventScopeNode.uastParent
+ while (currentNode != null) {
+ // Check if awaitPointerEventScope is within a PointerInputEventHandler or a
+ // pointerInput method call (making it a valid use of return).
+ if (
+ currentNode is UCallExpression &&
+ (currentNode.methodName == POINTER_INPUT_HANDLER ||
+ currentNode.methodName == POINTER_INPUT_METHOD ||
+ currentNode.methodName == COROUTINE_METHOD)
+ ) {
+ return true
+ }
+
+ // For backward compatibility, checks if awaitPointerEventScopeNode is returned to a
+ // "suspend PointerInputScope.() -> Unit" type variable (see test
+ // awaitPointerEventScope_assignedFromContainingLambdaMethod_shouldNotWarn() ).
+ if (currentNode is UVariable) {
+ val variable = currentNode
+ val lambda: UExpression? = variable.uastInitializer
+
+ // Check if the initializer is a suspend lambda with the correct type
+ if (lambda is ULambdaExpression) {
+ val ktLambdaExpression = lambda.sourcePsi
+ if (
+ ktLambdaExpression is KtLambdaExpression &&
+ isSuspendPointerInputLambda(ktLambdaExpression)
+ ) {
+ return true
+ }
+ }
+ }
+ currentNode = currentNode.uastParent
+ }
+ return false
+ }
+
+ // Helper function for lambda type check
+ private fun isSuspendPointerInputLambda(ktLambdaExpression: KtLambdaExpression): Boolean {
+ return analyze(ktLambdaExpression) {
+ val type = ktLambdaExpression.getExpectedType() as? KtFunctionalType ?: return false
+ type.isSuspendFunctionType &&
+ type.receiverType?.expandedClassSymbol?.classIdIfNonLocal?.asFqNameString() ==
+ POINTER_INPUT_SCOPE
+ }
+ }
+
companion object {
+ private const val POINTER_INPUT_SCOPE =
+ "androidx.compose.ui.input.pointer.PointerInputScope"
+ private const val POINTER_INPUT_HANDLER = "PointerInputEventHandler"
+ private const val POINTER_INPUT_METHOD = "pointerInput"
+ private const val COROUTINE_METHOD = "coroutineScope"
+
const val IssueId: String = "ReturnFromAwaitPointerEventScope"
const val ErrorMessage =
"Returning from awaitPointerEventScope may cause some input " + "events to be dropped"
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/CoroutineStubs.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/CoroutineStubs.kt
new file mode 100644
index 0000000..1165b64
--- /dev/null
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/CoroutineStubs.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.ui.lint
+
+import androidx.compose.lint.test.bytecodeStub
+import com.android.tools.lint.checks.infrastructure.TestFile
+
+object CoroutineStubs {
+
+ val coroutineContextTestFile: TestFile =
+ bytecodeStub(
+ filename = "CoroutineContext.kt",
+ filepath = "kotlinx/coroutines",
+ checksum = 0x2eedf37a,
+ source =
+ """
+ package kotlinx.coroutines
+
+ public interface CoroutineContext {
+ public operator fun plus(context: CoroutineContext): CoroutineContext = context
+ public operator fun <E : CoroutineContext.Element> get(key: CoroutineContext.Key<E>): E? = null
+ public interface Key<E : Element>
+ public interface Element : CoroutineContext {
+ public val key: Key<*>
+ }
+ }
+ """
+ .trimIndent(),
+ """
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAA/2WNQQvCMAyFI4pgDyL9AYIiHjz07H04kV2E+QfGVmdwJqVN
+ wZ9vy/Rk4IXHC3kfAEwBYJK0gO+oo9o11HnG7m1afjkO1tw5UtcIMpneBone
+ Br0q2Z+a9nEeg0pUoTZ/nxENkotiHCOJ9Xpdx+AsdUj9dYwu+V7ikGwq2Sv9
+ ZBmQcofnKEgZVvx83bLLsKWaSSLr+S3tSrZwgA8/eWd6zwAAAA==
+ """,
+ """
+ kotlinx/coroutines/CoroutineContext$DefaultImpls.class:
+ H4sIAAAAAAAA/61UW08TQRT+poVui0VuUgUULxRprbLg5YUSElMx2VjRCOHF
+ p+kylml3Z3F3toEH/4C/xkfjgz/AH2U8066I9ZIl4WHPbb7znZ1zZubb9y9f
+ ATzGE4a1bqA9qY5tNwiDWEslIrvx02wESotjXX4m3vHY045/5EUWGMNkh/e4
+ 7XHVtl+1OsLVFrIMI0deHDG8rzRTkNZTgaqpUAxLzSBs2x2hWyGXKrK5UoHm
+ WgZk7wR6J/Y8QlnuICGPPMPigNnu9HxbUjhU3LMdpUPKly7tc4xh1j0Ubjch
+ eM1D7gsCMqxUmsMdqJ+J7BqSdr26X0QR42O4hMsMo2V9KKk9yym3lG0LzfDh
+ 4ppZfiFO0jW0vO0JXyjzFx83tzc2zpWzdbF/vLm3Xd+qV0kylP83ZZoQb3nC
+ dK4rTvIoMaTbrGkLw1SCtV8KzQ+45hTL+L0sXRRmRMEIMLCuMei0Z46lsdbo
+ 8KWoYuEOQ/HsRWKonaOtFpbp/CYOHcCUG7NQoX6QwTAzvL7aJaKRRnAgGCaa
+ FN+J/ZYI90wXGaabgcu9fR5K4yfBhTex0tIXjurJSFLo6a8J0HiGV09vzG+w
+ wq5sK67jkAiLjlIibHg8igQtje0GceiK59IUm0vo9v8ohXVkMGLmQbqAUeSQ
+ xX3yNsnPkM7XpgufMVH71Ec8IJmj0eUoZ5Xs0gCDSUz1Oeg5IIvBTnCWGSt9
+ 45nEGcgCpjFDFEOFrlIhNlxo9B+FSrhG6+t99JXTgrN/L5jFQ5LzpDNYQhlz
+ uEt6JVdAlfQ90o/6RDV6yoFFypoj7PxbZB0sOLju4AYWHdzELQe3fwB+y2cT
+ 9QUAAA==
+ """,
+ """
+ kotlinx/coroutines/CoroutineContext$Element$DefaultImpls.class:
+ H4sIAAAAAAAA/7VUS1MTQRD+JoFsEoPhISiiUSEIIcKCoiJBqqyI1pYRLaG4
+ eJosY9hkdxZ3Z1Nw8Ogf8Jd4pDxYnv1Rlr1J5BEta2PhYfu13V/3dPfM9x9f
+ vgJYxjrDSsNVtiUPdNP13EBZUvh6+ZdYdqUSByq/YQtHSJV/Kt7xwFaGs2/7
+ GhjDYJ03uW5zWdNfVevCVBriDH37duAzHM5WegAvRXEuFSJ5MUxVXK+m14Wq
+ etySvs6ldBVXlkvypqs2A9smL81sBySRZMi1kfV609EtMnuS27ohlUfxlknn
+ TTOMmnvCbHQAXnOPO4IcGWZmK92dKJ2ybIUgtVJhJ4MMBtK4gItUYoSTaBhk
+ WIzSxbOjGWZ4H6n759n1DPpxKY0RjDL059WeRTsw39MKMExHnG+8JhTDx/Pf
+ sPwLcRjtvKeq/rS2sbraU8z6/6l8bXujtF4qEGXI/+0K0Pryqi3CTjbEYRI5
+ hg/ntzD/0MQMruJmuDy3GKJFhjkYhjq++kuh+C5XnGwxpxmn942FJBUSMLBG
+ KNDjFDuwQmmRIXP6zjAUeyhXwwK9Hh2Frn/EejUsUcNJYBjp/r/QIKC+srsr
+ GLIVsm8GTlV42+GYGIYrrsntHe5Zod4xTrwJpLIcYcim5VtkenIyYpp/99/j
+ 9+qMW2rLqkmuAo8AM4aUwivb3PcF/UpvuYFnimdWmGy8A7fzWyosIYa+sM3E
+ U/QIJBDHfdKekx4jni0Op46QnfuGoeIRxj63HB8QTRNPQMNFCntI+ljbHZdx
+ pQWXRRLjNLwVkhPENeKP6BuIdZQ2TdHuTFAFXTlvnOSc7CVnDlMEvNqKuHac
+ +/qfc8dRIjpJ2UcwhHkqVye+mEhRsN7S75J+j/S1FuAyHhPPUXSeYqffIm7g
+ toEZA7MoGJhD0cCdn3hWRYahBwAA
+ """,
+ """
+ kotlinx/coroutines/CoroutineContext$Element.class:
+ H4sIAAAAAAAA/5VSTW/TQBB9aye2EwqkKZQk5askpXyIOlQcQHxIKIBkUYrU
+ ShVSTpt0G22yWSPvOgq3/BYO/AgOKOqRH4UYhyAQXNyDZ968nZl9ntnvP75+
+ A/AI2wz3R7FVUk/DfpzEqZVamLDzG3ZibcXUtl4rMRba+mAMlSGf8FBxPQjf
+ 94aiT6zL0MzRxkeRwRsI+1Z8oovv3N3LczclP2XYyZ/97N4LKmjuxckgHArb
+ S7jUJuRax5ZbGRPej+1+qhRlrS6bhu+E5cfccuKc8cSl6bDMlDIDBjYifiqz
+ qE3o+CHDk/msWnZqzuKbz37BwAlOavPZrtNmB+sVp+FVncBtux9OvxROP3te
+ oxAUKsWswS7Dg1w/tBw96drKk0957igb71mG6y8vYXh8BkmtV+KEp8pG44/K
+ +NhgWPmbYdjOqcDHdVK9eBRr/57vjEhV6VAONLdpIhg2DlJt5VhEeiKN7Cnx
+ 8s9iSUGktUg6ihsjKCwfxmnSF2+kosr6svLovzqPNoICrdnLll2ggSAg5hZF
+ DkpokvfotEzeRYtsg7xDXMMr4Sqha6jjBsU3Kd5aVG3iNvnnVHUORax04UY4
+ H+FChIuoEMRqhCrWumAGl3C5C89g3eCKQc2gbuAbBD8B9Tm4oacDAAA=
+ """,
+ """
+ kotlinx/coroutines/CoroutineContext$Key.class:
+ H4sIAAAAAAAA/5VQy0oDQRCs3o3ZJL7iO74FBVHRjUEQoggSIwQVQcFLTpM4
+ ypjNLGQmEm/7XR5kz36U2BvjxVOcQ1d1TddQPZ9f7x8AjrBB2G6FNlC65zfD
+ Tti1SkvjV35pJdRW9uzWlXzzQISL02q5fD2MoxrIttT25Oz6RbwKPxD62b9t
+ vMimPSHk/2oeUoSpwcP+jbTiUVjBk0771eWklJRsUkCgFus9lXRFZo+HvEQc
+ TeScgpOLoz44+TjKPBXiaDeTiaM87TpFp+QU3WS8RNgZagVemhNQlbD/r5Up
+ Cbn3D4eHOcLmEAYPCwRv4CK4nJAw83fqoMV3YzWtZacSCGOkIWTv1bMWttuR
+ hNx92O005aUKuFm862qr2vJBGdUI5LnWoRVWhdqk+bMwgp+TwgrXaVbmUcBi
+ OsusgCXGVdYduFjr4zLWGY/5A9Ls9epwa8jUkK0hh1GmnAvjmKiDDCaRryNl
+ MGUwbTBjMPsNX1GO1pgCAAA=
+ """,
+ """
+ kotlinx/coroutines/CoroutineContext.class:
+ H4sIAAAAAAAA/5VSW08TQRT+Zrbd3ZaqW/BSioJCkZuwlfgEhMRgDQsVjBhj
+ 0qehDM3S7S7ZmSXw1l/hD/Bn+GAaHv1RxrOAN0zM8jDn+p3vnMk5375/+Qrg
+ BZYYprqRDvzw1G1HcZRoP5TK3fhpbkShlqfaAmNwjsSJcAMRdtzd/SPZpqjB
+ kDsOEsXQnG1m4Fmdy4SioZpR3HGPpN6PhR8qV4RhpIX2I7J3Ir2TBAGhjI7U
+ DB8zta5ty7Ns7WuNQPZkmI4h1xorKzeqWc88zNr7xur66hxJhtr/vkt/FfuB
+ JFj5itt9I7U4EFpQjPdODFolS0UhFWBgXYqf+qlXJ+vgOUM06FeLvMKL3Bn0
+ i9zmF05qpjE7bx9WBv1lXmcrvDi0Nevwaq7CNo1J2x70HWOe13PLJSdftYdz
+ w3zTqJub55/4+WeTO9ZW2bGrvE7VplO4MNJc2naZYTrjvtOhGwyLN9yP0ZVn
+ DNm2mu6fwWpfumRd0RDLdspSz0LySh6KJNBe7zhQFh4zlP6MMCzcYH4LUwwz
+ GUe3MM0wcj2z1KXxx94lofZ70gtPfOXTpbz8fT10Wtezb0UselLL+C9YYc/v
+ hEInsaQ/eWEo441AKCUpVdyLkrgtX/sB5Uav6D7808qklSOXHiB4jiEPk/xZ
+ 8kxarkV6jp7FLh0C2CjAwHwKR/EXcOgakCALJKuknxBwEqOokR43C3hKeoL0
+ swuKGSyS3qWqEvW+1YLh4baHOx4clMnEsIcR3G2BKdzD/RZKCg8UKgq2wqhC
+ VWFM4aFCQSGv8EhhXGHiBxzlJBgpBQAA
+ """
+ )
+
+ val coroutineScopeTestFile: TestFile =
+ bytecodeStub(
+ filename = "CoroutineScope.kt",
+ filepath = "kotlinx/coroutines",
+ checksum = 0x2041f1c4,
+ source =
+ """
+ package kotlinx.coroutines
+
+ public interface CoroutineScope {
+ public val coroutineContext: CoroutineContext
+ }
+
+ class ContextScopeImp(context: CoroutineContext) : CoroutineScope {
+ override val coroutineContext: CoroutineContext = context
+ }
+
+
+ public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
+ val newScope = ContextScopeImp(EmptyCoroutineContext)
+ return newScope.block()
+ }
+
+ public object EmptyCoroutineContext : CoroutineContext
+ """
+ .trimIndent(),
+ """
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAA/2WNQQvCMAyFI4pgDyL9AYIiHjz07H04kV2E+QfGVmdwJqVN
+ wZ9vy/Rk4IXHC3kfAEwBYJK0gO+oo9o11HnG7m1afjkO1tw5UtcIMpneBone
+ Br0q2Z+a9nEeg0pUoTZ/nxENkotiHCOJ9Xpdx+AsdUj9dYwu+V7ikGwq2Sv9
+ ZBmQcofnKEgZVvx83bLLsKWaSSLr+S3tSrZwgA8/eWd6zwAAAA==
+ """,
+ """
+ kotlinx/coroutines/ContextScopeImp.class:
+ H4sIAAAAAAAA/41SXU8TQRQ9sy3dZS10KR+WgihfUoqyhfiGMVESkk0qGjDE
+ hKfpsinTbnfJ7rThkV/hD/AXaKLR+GAIj/4o453tBm0lysPMvffknnPn3js/
+ fn77DuAJthiW2qH0RXBuu2EUdqUIvNjeDQPpnctDNzzznM6ZDsZgtXiP2z4P
+ mvarRstzpY4Mw+KN7NRN+DpGGHJPRSDkM4ZKpf4vRlp4Z/2IYbkeRk275clG
+ xEUQ2zwIQsmlCMnfD+V+1/d3GHS3TzFgMiz0te1Wr2MLgqOA+7YTyIj4wo11
+ 5Bmm3VPPbacCr3nEOx4lMqxV6sMd7vyBHCqRJj0sj3EUTIzBYshUVKyjaCKL
+ SZqRO9QIw+qt+s1jGjOj0HCXIStPRcywcjNxYDHU/2TTk8Nyqpn1W9VlmEjz
+ 7Jee5CdccsK0Ti9D34Opa1RdYGBtws+FimrkndDP2bu8KJpaSUvO5UXfNbKl
+ y4ttrcZejBVzllbWapmr9znNyh5MWSMqenv1LkuIQYxy1shZulLbVuv+71dS
+ zx1ENtvU7NxBN5Ci4zlBT8Si4XvPf/8UGudueOIxFOrE2e92Gl70hlMOQ7Ee
+ utw/4pFQcQquDGtd/5ABUfMw7EautycUZzblHP1VHVu00ixNL0enqHZMtkoj
+ VPEoWY2sDoPsBkV1shpZa6N45ysmql8wVd34jNLHJPMR3ePIEFeHSZwC2ceE
+ zfQ5mEVZrYo8M6mzST4Nt1+IpoT5tIyt1kl2pPoJpQ/X2rkENBLNfD8h1Rx8
+ sZ3c66iR3SX0HuUtHCPj4L6DBw4WsUQulh2sYPUYLMZDrB3DiFGOUYlhxpiJ
+ VTgdYy7G/C9SSg1BjQQAAA==
+ """,
+ """
+ kotlinx/coroutines/CoroutineScope.class:
+ H4sIAAAAAAAA/41QTWsbMRB9ktdrZ5Mma7tN/HEqJSS9dF1Teik9FENhwXXA
+ hhDwSd6oRvZGKivZ+Ojf0kN/RA/F5NgfVTpr3IZgSAJi5s3w5mnm/f7z8xeA
+ d2gxvJwZlyq9jBKTmblTWtqo+w8OE/NNlsAYwqlYiCgVehJdjKcycSUUGGoT
+ 6f6Tu0Y7uXQMZ+evew+pbokfGF71TDaJptKNM6G0jYTWxgmnDOG+cf15mhKr
+ slWLvkgnroUT1OM3iwKdwPKwlwcwsBn1lyqv2oSu3zK8X6+qAa/zgIfrVUBv
+ g8u8/LW+XnV4mw2qIW/yduHq9od3+933m17ZC4v5dCdf71FzaBX6OUx2TDh9
+ ogWV+3pvZjTbGsy1Uzcy1gtl1TiVn+58YQiGZp4l8rNKJUNjS73cIfp0BDxs
+ rPEYivBp0TpVeS5R5mhs4gmalD9St0ysvREKMYIY+zEO8IwgDmMcIRyBWVRQ
+ HcG3qFk8t3hhcWxRtPD/AoCSp91SAgAA
+ """,
+ """
+ kotlinx/coroutines/CoroutineScopeKt.class:
+ H4sIAAAAAAAA/41TW08TURD+zrb0iqXUS7koIq1SrlsQFWkhEgKhsVTTNiSE
+ p+2ykqXtLtk9rfDGkz/EX2B8MWpiGh598hcZ5yytUFqp2ew538x8c2bmzJmf
+ v7/9ALCENYZY2eQV3TiRVdMya1w3NFveaMGCah5rr7kXjCF8pNQVuaIYh/Kb
+ 0pGmktbFEFLbuAzFRPbiRPmoXpXf1QyV66Zhy1tNtJhq2dsCGgRqiiCkprLX
+ I6UYfqXzK536tV7B0nPZnvWl5npklJ4r5lNrqZku8XsV03TtWlI8a1qH8pHG
+ S5aiU9KKYZhcuSggV6tUlFJFI1rsJprJBZNYE13LNLh2wp0iM9VjLwIMiS68
+ zeoxP/17J00vL/oZfJlcobie29hkmO52kV09U/0IYcCPWwj3eF5/Q0UYPGnd
+ 0Dk9yETixpa1gkzt9uMO7gYQxD2GsZufgRdDFEE36maZnuhyorMdnZouPevH
+ CEaDGMZ9uhpDe9988vHuCbddPrWor1Qx1TLDeK8BYQjGVbN6XNGEfMXhXxPD
+ MNii7GhcOVC4QjqpWnfRlDOx+MUCBlYWQCLjiS5QktDBAoPaOBsJSD5XQBqS
+ Lv9w48wBZFwlPCJtswm3r3EWZouTYWkk5mMRd0RKuiOBiE+gbZbsi3gi7iGW
+ 9CRd5x89ks+7ff7hlUBhnwi1SCnkGSKtdK8WHfuPUWWiiLGW9+YJ1wyb3FvH
+ FE8dzmC713yZM7g3zAPq1UCWtLlataRZRTFgIhdTVSq7iqULuan0F/RDQ+E1
+ i/BovkZ3XdUyRl23dTKvX04gNf+69a1iKVWNa1YbLVAwa5aqbeni9OGmz27H
+ eViABDcumjWMPnhIek7SHlwQDYt+R3DvMwYbuP0F0Z3p2ZmveODCJ9FVvKA1
+ QDto9kL0LRMaJ7cQ/BjDQ8cSJc0jJ0AUE4hRmJeOrxcrtHtI9tGeEmdIJPid
+ TPwOJe2sz7BK+xZp45Tf4324MniSwWQGCUxlMI2ZDGYxtw9mYx7yPvw2+mwk
+ bYxTeTYWbcRsPLWxZMPzBwVwSvaKBgAA
+ """,
+ """
+ kotlinx/coroutines/EmptyCoroutineContext.class:
+ H4sIAAAAAAAA/61VW08TQRT+ZgvdtixyUbl6p0oLyhbEG60o1ppsqNVYQjQ8
+ DWXEhe0u7k4JPJjw5A/xF0h8wGhiiL75o4xnSkWhxCzGh5kz58z5zjl7vpnZ
+ 7z8+fQEwiRmG1KonHdvdMCue79Wk7YrALFTX5Gb+l573XCk2pA7G0LnC17np
+ cHfZfLK4IipkjTAMHRGjGd7KEM3Zri2nGSKp9LwBHbEEWhBnaJGv7IBhpBi2
+ mixh1pwaYYqpo0BN/ulQXvQtRc9fNleEXPS57QYmd11Pcml7tC55slRzHPLK
+ hAiWfChe8pojreqaE+joZHgdrtT/+D0G2tGdQBdOMuiVPSPD5ZC9iCwL8n4e
+ qurkrNgMV1Sy4IiqcFUCkStMTR0LMx26mNxcITudTdPMkPwbqcQoX3QEub35
+ f/z8QzsM9GJQcXWGOr8qNhnC4VUmhliu4tQvVwKaulExq1SemynlCwYuwYiT
+ cYihqxHQfCwkX+KSE1CrrkfoMWBqiqsJDGyV7Bu20jK0Whqne7G7lUhofVp9
+ 7G7Fvr3V+na3JrQMe6DHtK/volqnplwnGIw/Dz7D6DGaoINS6Q2FYTjk9+uY
+ pJ7Nqp517e+XK96aGFulMIPPaq60q8Jy1+3AJq5nfvNP70jeWxIMHUXClGrV
+ ReHPqfPA0F30KtyZ576t9IYxeTjWU+7zqpDCPxA0XraXXS5rPkEMy3WFn3d4
+ EAjaSpS9ml8Rj2wVrr8Rbr6pMIwTZy1ERpRGvyKR5G1iROkdJFton15Q0u6Q
+ ZiraSLaO7CCxTQsNUw1n0JnK0mzsOaCNVorldpxApA6eIm9NeY+MfsSp90ei
+ e/Y8Gmi1Ok22gyXlaOisoTA6z31UZVOCs2ETnMN52r9b9+7fTzRwOFEMF/bb
+ 0FsPArR9hvZiBxc/ILldN0QwTfMAyS6KkqF4EySvR+O4QfImyXv1PLdwv/5r
+ pFeSWnVlARELwxZSFtIYoSVGLVzFtQWwAGMwaT+AEaAnQOwn9GRn5FcHAAA=
+ """
+ )
+}
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetectorTest.kt
index a562f5f..8a69674 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetectorTest.kt
@@ -87,6 +87,8 @@
UiStubs.PointerEvent,
ForEachGestureStub,
UiStubs.Alignment,
+ CoroutineStubs.coroutineContextTestFile,
+ CoroutineStubs.coroutineScopeTestFile
)
@Test
@@ -110,6 +112,126 @@
)
}
+ // Current way to create a handler for the pointer input type
+ @Test
+ fun awaitPointerEventScope_assignedFromContainingPointerInputEventHandler_shouldNotWarn() {
+ expectClean(
+ """
+ package test
+ import androidx.compose.runtime.Composable
+ import androidx.compose.ui.input.pointer.PointerInputEventHandler
+
+ @Composable
+ fun SomeFunction() {
+ val blockNew = PointerInputEventHandler {
+ awaitPointerEventScope { }
+ }
+ }
+ """
+ )
+ }
+
+ // Current way to create a handler for the pointer input type
+ @Test
+ fun awaitPointerEventScope_assignedFromContainingPointerInputEventHandlerAfter_shouldNotWarn() {
+ expectClean(
+ """
+ package test
+ import androidx.compose.runtime.Composable
+ import androidx.compose.ui.input.pointer.PointerInputEventHandler
+
+ @Composable
+ fun SomeFunction() {
+ val blockNew = PointerInputEventHandler {
+ awaitPointerEventScope { }
+ val something = "hello"
+ }
+ }
+ """
+ )
+ }
+
+ // Current way to create a handler for the pointer input type with variables above
+ // awaitPointerEventScope.
+ @Test
+ fun awaitPointerEventScope_assignedFromContainingPointerInputEventHandlerVars_shouldNotWarn() {
+ expectClean(
+ """
+ package test
+ import androidx.compose.runtime.Composable
+ import androidx.compose.ui.input.pointer.PointerInputEventHandler
+
+ @Composable
+ fun SomeFunction() {
+ val blockNew = PointerInputEventHandler {
+ var variable1 = 0
+ var variable2 = "hello"
+ var variable3 = 0.0
+ var variable4 = "hello2"
+ awaitPointerEventScope { }
+ }
+ }
+ """
+ )
+ }
+
+ @Test
+ fun awaitPointerEventScope_assignedFromContainingLambdaMethod_shouldNotWarn() {
+ expectClean(
+ """
+ package test
+ import androidx.compose.runtime.Composable
+ import androidx.compose.ui.input.pointer.PointerInputScope
+
+ @Composable
+ fun SomeFunction() {
+ val block: suspend PointerInputScope.() -> Unit = {
+ awaitPointerEventScope { }
+ }
+ }
+ """
+ )
+ }
+
+ @Test
+ fun awaitPointerEventScope_inCoroutineScopeStandaloneExtensionFunction_shouldNotWarn() {
+ expectClean(
+ """
+ package test
+ import androidx.compose.ui.input.pointer.PointerInputScope
+ import kotlinx.coroutines.coroutineScope
+
+ suspend fun PointerInputScope.detectMoves() = coroutineScope {
+ awaitPointerEventScope { }
+ }
+ """
+ )
+ }
+
+ // Pointer input handler implicitly using PointerInputEventHandler for pointer input handler.
+ @Test
+ fun awaitPointerEventScope_standaloneExtensionFunction_shouldNotWarn() {
+ expectClean(
+ """
+ package test
+ import android.view.MotionEvent
+ import androidx.compose.ui.Modifier
+ import androidx.compose.ui.input.pointer.pointerInput
+
+ fun Modifier.motionEventSpy(watcher: (motionEvent: MotionEvent) -> Unit): Modifier =
+ this.pointerInput(watcher) {
+ interceptOutOfBoundsChildEvents = true
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent(PointerEventPass.Initial)
+ event.motionEvent?.let(watcher)
+ }
+ }
+ }
+ """
+ )
+ }
+
@Test
fun awaitPointerEventScope_insideForEach_shouldNotWarn() {
expectClean(
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt
index a3043ec..38e9d10 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt
@@ -117,11 +117,12 @@
"""
)
+ // Functional Interface for pointer input event handling
val PointerInputScope: TestFile =
bytecodeStub(
filename = "SuspendingPointerInputFilter.kt",
filepath = "androidx/compose/ui/input/pointer",
- checksum = 0xa05e1a0a,
+ checksum = 0x9c1bd41d,
"""
package androidx.compose.ui.input.pointer
import androidx.compose.ui.unit.Density
@@ -133,58 +134,75 @@
): R
}
+ fun interface PointerInputEventHandler {
+ suspend operator fun PointerInputScope.invoke()
+ }
+
fun Modifier.pointerInput(
key1: Any?,
- block: suspend PointerInputScope.() -> Unit
+ block: PointerInputEventHandler
): Modifier = Modifier
- """,
- """
- META-INF/main.kotlin_module:
- H4sIAAAAAAAA/2VMQQoCMRAbUQR7EOkDBMWThzl7F1dkL4J+oGzr7oDOlHYK
- Pt/KejOQEBISAJgCwKRyAT+Yg9k59knIv7GTV5Qc8CGFvVMSxj5kLSlku2ok
- nVw3nMegVXM0m79lISSORTEKsYZk17eSY2BP3F/H6PLtG3pWW0+WZqb10c7v
- VVvdwh4+IfeLY6cAAAA=
- """,
- """
- androidx/compose/ui/input/pointer/PointerInputScope.class:
- H4sIAAAAAAAA/51Ty24TMRS9nsljklKYDlDSFtqSlpdQmRBg04SKClo1VYAq
- idh05UzcyOnEjsae0O7yLSz4CBYo6pIf4S8Qd/JQSxsRxML28fXx9T328Y9f
- 374DwEt4SOAFFY1A8saJ68l2RyrmhtzlohNqtyO50CxwD4ZjKQpWPdlhSSAE
- 7BbtUtenoul+rLeYp5NgElidlC4UXLvvmFBcnyYhTmCefqZcj/LudJkY5iVQ
- e1w+ltrnwm112+5RKDzNpVDu7gjlC+N1TwYy1Fww5b6VAkFII0LhSflyXQUC
- P4uVzavxrWmHFTfK029ne6KUwsaUQosbtUphq/B0QlnTNI62TlS6XpZB020x
- XQ8oRy1UCKnpUNeH0Pdp3WdIW/sbTeqIiay5cSHvmaYNqinGjHbXRO+QqEtF
- HRAgxxg/4dEsh6jxnIDq97JpI2MMm2We46jZ/d4A9HvWUabfyxs5sv/aNhaN
- PTMbs/o928w/smOLaxZxYo6RSzhpx4rQnplLOgknliE5Kxc/+5IwrNTe2dc3
- EbLT0dF5Aq/+4dGuWBqVZSfuu+hdJJEKgXjdlx5KdsbXc+5OApv/bxn8PdPc
- TaLrXh6zdk50VJgU4wJqp4M0K9VQdZhocNG8KHSX+wifHWsCqSpvCqrDAP/c
- UiXEE9qsJLpccfTH9rkZ0FCXVw9oQNsME/1BS1dlGHgMj8CMC6M9n67kS+Ab
- QQw1JCLjxAgkwQIT7uPMgBRkcUzgahrHNWyzBk5mBh6bGVDWB/0qPMCxjNFr
- EIfZQzBLcL0EN0pgwxxCcEpwE24dAlFwG+YPYUbBHQUZBUkFCwoWFSwpuKvg
- noJlBSsKrN90t5jzEwUAAA==
- """,
- """
- androidx/compose/ui/input/pointer/SuspendingPointerInputFilterKt.class:
- H4sIAAAAAAAA/61VW28bRRT+Zu34VkPcDSmJE9KUmObSpusYym2jiBJRycIN
- BZe85Gm8nroTr2esnd0ofetv4RcgnhAPKOKR38JvQJzZOKnTBAehPuyZc/nm
- 3OYc+8+/f/sdwCfYZviKq26kZffYC/RgqI3wEulJNUxib6ilikXktRMzFKor
- Ve/pqaZpzY9lSOy3cR6MoXLIj7gXctXzvuscioC0GYbycAzPMFxrXRXsie7K
- 51JEfutNJ36rr+NQKu/waOA9T1QQS62M93jENfz1yQ4Z/nrbIbc3r3R4sWHj
- bWoHeij8zTO3gY50EksljLerFTEJt363zwE/Khn7O/69y5ntXF/uSktHPe9Q
- xJ2IS0qbK6VjflrCno73kjAkVG0SiiC8EwqC5bbjF9LsFFBiWBrrSlqb4qHX
- VHFE92Vg8igzzAYvRNAfhXnKIz4QBGRYXbuiy681beuk56/vl/Eupkt4BxWG
- qU6og34BLsPipJrzeI+huEsGrih/hsnvXTtH+mXcwvtFzGKOwa3ZSmsXp3Xp
- umZn++LlFl2+XB3D8nWTy3DzDPJExLzLY046Z3CUocVklhQtAQPrW8Yh47G0
- XJ24LsU1J69qpZNXJWfOSY8KkZF4/lWckbn6NZmrTp1t0NcoVJxqdo7VM43V
- Sra6UmBu1nXqObfkFlKuUM+7OTdF1Kf++CnnFIqWVko2dIPZrNyz7MdLevi/
- dmOsW/+yHb59jRHkm+NY0MxpdRb42cvUx93/9u5527/bk37RHvTp8bO7uisY
- pluUy14y6Ijomd0KW7cOeLjPI2nlkbLYlj3F4yQifuGHhBIfiKY6kkaS+dHr
- 7aLVe9N6vicXYOWmUiLaDbkxgsRSWydRICg/CjA/crF/yT224CCL07GZxxRy
- JH1M0vekt6Mzs+He+BU377szRH/B/Almf7azRf8EIDDFxTQeEr98CkcVC6m7
- GSziA7JbzsUS3fg0vZfHZ6ObBTo/t/bMSKCupNTyGXxBtETSFi3cLcrty/R6
- Az6dLdLfpmyXD5Bp4k4THzaxgloTH+FuE6tYOwAzWMfGAW4YTBksGCwa3DNw
- De4bbBo8SEXPIPcPJkHsLNsGAAA=
"""
+ .trimIndent(),
+ """
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAA/2VMQQoCMRAbUQR7EOkDBMWThzl7F1dkL4J+oGzr7oDOlHYK
+ Pt/KejOQEBISAJgCwKRyAT+Yg9k59knIv7GTV5Qc8CGFvVMSxj5kLSlku2ok
+ nVw3nMegVXM0m79lISSORTEKsYZk17eSY2BP3F/H6PLtG3pWW0+WZqb10c7v
+ VVvdwh4+IfeLY6cAAAA=
+ """,
+ """
+ androidx/compose/ui/input/pointer/PointerInputEventHandler.class:
+ H4sIAAAAAAAA/7VSTW8TMRB9dpJNs1DYlgJpy8cBDoAEDhVcSIsECNRFAapG
+ cMnJSazIycaO1t6o3PJbOPRHcEBRuPGjELMLFaUgpCIh2fPlN/PsGX/5+vET
+ gAe4xvBImn5qdf9A9Ox4Yp0SmRbaTDIvJlYbr1Kx913HefD5VBm/SzmJSqtg
+ DNFQTqVIpBmIN92h6vkqSgyBNlM7Ugz6Vut0BO2enahma2R9og2lpDbz2ign
+ nllDRia9tqZ5u3WStsnw/r9wbd89Arw12jcf/5n7ZsumAzFUvptKbZyQxlhf
+ 5DvxOksS2U0UwW78DWZ9jiTUyhHjK+VlX3pJMT6elmhmLBe1XICBjSh+oHOv
+ QVb/PsPOfLYW8joPeTSfhbR4xAt/Plv6fLiYsfp8tsUb7OVqxDd4o3SHN8q7
+ i8Oniw8Bjyp5kS2G7VP28finoKvSzR7+0yQYrrczN1Gmr83g+PELnZB5b+QZ
+ am09MNJnKX2uzf2M5jRWsZlqp6nDT362k0Zy8nRPpnKsqNAvsLBts7SniIIq
+ rv/IefdbvYB6gzKKxpcZKghQwjp5HFVskA7o2UukN2kvc3JqxYxqBeRKIeu4
+ SnqHoiEVONNBKcbZGMsxzuE8mYhirGC1A+ZwAWsdBA4XHS45VAp52SH4BreP
+ UBy8AwAA
+ """,
+ """
+ androidx/compose/ui/input/pointer/PointerInputScope.class:
+ H4sIAAAAAAAA/51Ty24TMRS9nsljklKYDlDSFtqSlpdQmRBg04SKClo1VYAq
+ idh05UzcyOnEjsae0O7yLSz4CBYo6pIf4S8Qd/JQSxsRxML28fXx9T328Y9f
+ 374DwEt4SOAFFY1A8saJ68l2RyrmhtzlohNqtyO50CxwD4ZjKQpWPdlhSSAE
+ 7BbtUtenoul+rLeYp5NgElidlC4UXLvvmFBcnyYhTmCefqZcj/LudJkY5iVQ
+ e1w+ltrnwm112+5RKDzNpVDu7gjlC+N1TwYy1Fww5b6VAkFII0LhSflyXQUC
+ P4uVzavxrWmHFTfK029ne6KUwsaUQosbtUphq/B0QlnTNI62TlS6XpZB020x
+ XQ8oRy1UCKnpUNeH0Pdp3WdIW/sbTeqIiay5cSHvmaYNqinGjHbXRO+QqEtF
+ HRAgxxg/4dEsh6jxnIDq97JpI2MMm2We46jZ/d4A9HvWUabfyxs5sv/aNhaN
+ PTMbs/o928w/smOLaxZxYo6RSzhpx4rQnplLOgknliE5Kxc/+5IwrNTe2dc3
+ EbLT0dF5Aq/+4dGuWBqVZSfuu+hdJJEKgXjdlx5KdsbXc+5OApv/bxn8PdPc
+ TaLrXh6zdk50VJgU4wJqp4M0K9VQdZhocNG8KHSX+wifHWsCqSpvCqrDAP/c
+ UiXEE9qsJLpccfTH9rkZ0FCXVw9oQNsME/1BS1dlGHgMj8CMC6M9n67kS+Ab
+ QQw1JCLjxAgkwQIT7uPMgBRkcUzgahrHNWyzBk5mBh6bGVDWB/0qPMCxjNFr
+ EIfZQzBLcL0EN0pgwxxCcEpwE24dAlFwG+YPYUbBHQUZBUkFCwoWFSwpuKvg
+ noJlBSsKrN90t5jzEwUAAA==
+ """,
+ """
+ androidx/compose/ui/input/pointer/SuspendingPointerInputFilterKt.class:
+ H4sIAAAAAAAA/6VTW08TQRT+pi29WaUtoFCuSuUmsqWiMSkaDdG4sSCK4YWn
+ 6XYs025nm51tg2/8AH+M8cn4YHj2RxnPtA33VBMf9sw53/nOZc6c/fX7x08A
+ G3jC8IKrqu/J6pHleM2Wp4XVlpZUrXZgtTypAuFbe23dEqoqVW23h9jG/Vq6
+ pL4NYmAM6TrvcMvlqma9q9SFQ2iYIdU6x2f4slS+rti2V5WfpPBL5ctJStfy
+ LzZ3vqVXHaGCNxTjUrblwcUY5sueX7PqIqj4XCptcaW8gAfSI33HC3barkus
+ /CAWUXjFFUSLbgaHUj+PI8Ew0/ACVyqr3mla3eYUdy1bBT7FS0fHcINhzDkU
+ TqNfZpf7vCmIyLC4dM0UzpA9k6RWWt5P4SZuJZHCMMNQxfWcRhwZhqlBd45h
+ hCGxRQ6uqH+Gwe+RP2WWUhjD7QRGcYchmzc3zV982pm/DTvSEJ/XKfjq7Rg2
+ /+eZGTLl/ry3RcCrPOCEhZqdMG04MyJhBBhYwyghch5JoxVIq1JPT0+Os8mT
+ 42RoPNQ90iR6Zm6K9FyowFboK8bToVxknBXCxWg6QuiQiS8yk3rh3+YYwwOG
+ 2UG/01qDhhnZ8qqCYbgsldhpNyvC/2i2zEzPc7i7z31p7D44+aGtAtkUtupI
+ LQl6ebahtL6Xvae7doGWspUS/pbLtRZkJve8tu8I6okKTPRT7F9Jj3WEEEFv
+ vBMYQhRhrJH1nnAz4pGVbPI70qvZLMlvGD/B6FfzBrBIRikohSwKpM/16JQk
+ 1003gklMkd9oGUxTxHo/IkZn0eDhvsEQ70qjh/GIZJKsVVrWMUq30S33EI/p
+ fEb4DHU5e4CwjTkbd23cw7yNPO7bWMDiAZjGEpYPENUY0shpTGqsaGQ0pv8A
+ cjqS8TUFAAA=
+ """
)
val Alignment: TestFile =
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index 9999552..4ca2049 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -387,9 +387,9 @@
}
public final class MouseInjectionScopeKt {
- method public static void animateAlong(androidx.compose.ui.test.MouseInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, optional long durationMillis);
- method public static void animateBy(androidx.compose.ui.test.MouseInjectionScope, long delta, optional long durationMillis);
- method public static void animateTo(androidx.compose.ui.test.MouseInjectionScope, long position, optional long durationMillis);
+ method public static void animateMoveAlong(androidx.compose.ui.test.MouseInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, optional long durationMillis);
+ method public static void animateMoveBy(androidx.compose.ui.test.MouseInjectionScope, long delta, optional long durationMillis);
+ method public static void animateMoveTo(androidx.compose.ui.test.MouseInjectionScope, long position, optional long durationMillis);
method public static void click(androidx.compose.ui.test.MouseInjectionScope, optional long position, optional int button);
method public static void doubleClick(androidx.compose.ui.test.MouseInjectionScope, optional long position, optional int button);
method public static void dragAndDrop(androidx.compose.ui.test.MouseInjectionScope, long start, long end, optional int button, optional long durationMillis);
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index 40c0b2c..e0e9e2d 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -388,9 +388,9 @@
}
public final class MouseInjectionScopeKt {
- method public static void animateAlong(androidx.compose.ui.test.MouseInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, optional long durationMillis);
- method public static void animateBy(androidx.compose.ui.test.MouseInjectionScope, long delta, optional long durationMillis);
- method public static void animateTo(androidx.compose.ui.test.MouseInjectionScope, long position, optional long durationMillis);
+ method public static void animateMoveAlong(androidx.compose.ui.test.MouseInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, optional long durationMillis);
+ method public static void animateMoveBy(androidx.compose.ui.test.MouseInjectionScope, long delta, optional long durationMillis);
+ method public static void animateMoveTo(androidx.compose.ui.test.MouseInjectionScope, long position, optional long durationMillis);
method public static void click(androidx.compose.ui.test.MouseInjectionScope, optional long position, optional int button);
method public static void doubleClick(androidx.compose.ui.test.MouseInjectionScope, optional long position, optional int button);
method public static void dragAndDrop(androidx.compose.ui.test.MouseInjectionScope, long start, long end, optional int button, optional long durationMillis);
diff --git a/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/MouseInjectionScopeSamples.kt b/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/MouseInjectionScopeSamples.kt
index 792ba856..a67ade9 100644
--- a/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/MouseInjectionScopeSamples.kt
+++ b/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/MouseInjectionScopeSamples.kt
@@ -19,8 +19,8 @@
import androidx.annotation.Sampled
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.ScrollWheel
-import androidx.compose.ui.test.animateAlong
-import androidx.compose.ui.test.animateTo
+import androidx.compose.ui.test.animateMoveAlong
+import androidx.compose.ui.test.animateMoveTo
import androidx.compose.ui.test.click
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performMouseInput
@@ -38,24 +38,24 @@
}
@Sampled
-fun mouseInputAnimateTo() {
+fun mouseInputAnimateMoveTo() {
composeTestRule.onNodeWithTag("myComponent").performMouseInput {
// Hover over the node, making an X shape
moveTo(topLeft)
- animateTo(bottomRight)
+ animateMoveTo(bottomRight)
// Note that an actual user wouldn't be able to instantly
// move from the bottom right to the top right
moveTo(topRight)
- animateTo(bottomLeft)
+ animateMoveTo(bottomLeft)
}
}
@Sampled
-fun mouseInputAnimateAlong() {
+fun mouseInputAnimateMoveAlong() {
composeTestRule.onNodeWithTag("myComponent").performMouseInput {
// Hover over the node, making a full circle with a radius of 100px
val r = 100f
- animateAlong(
+ animateMoveAlong(
curve = {
val angle = 2 * PI * it / 1000
center + Offset(r * cos(angle).toFloat(), r * sin(angle).toFloat())
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToNodeTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToNodeTest.kt
index 653217f..0f442e20 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToNodeTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/ScrollToNodeTest.kt
@@ -25,8 +25,12 @@
import androidx.compose.testutils.expectError
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
@@ -62,7 +66,8 @@
val orientation: Orientation,
val reverseLayout: Boolean,
val viewportSize: ViewportSize,
- val targetPosition: StartPosition
+ val targetPosition: StartPosition,
+ val hasNestedScrollConsumer: Boolean
) {
val viewportSizePx: Int
get() = viewportSize.sizePx
@@ -112,8 +117,10 @@
"targetIs=" +
when (targetPosition) {
NotInList -> "$targetPosition"
- else -> "${targetPosition}Viewport"
- }
+ else -> "${targetPosition}Viewport "
+ } +
+ "nestedScrollConsumer=" +
+ hasNestedScrollConsumer
}
companion object {
@@ -132,13 +139,16 @@
for (reverseScrolling in listOf(false, true)) {
for (viewportSize in ViewportSize.values()) {
for (targetPosition in StartPosition.values()) {
- TestConfig(
- orientation = orientation,
- reverseLayout = reverseScrolling,
- viewportSize = viewportSize,
- targetPosition = targetPosition
- )
- .also { add(it) }
+ for (nestedScrollConsumer in listOf(true, false)) {
+ TestConfig(
+ orientation = orientation,
+ reverseLayout = reverseScrolling,
+ viewportSize = viewportSize,
+ targetPosition = targetPosition,
+ hasNestedScrollConsumer = nestedScrollConsumer
+ )
+ .also { add(it) }
+ }
}
}
}
@@ -257,6 +267,11 @@
Modifier.composed {
with(LocalDensity.current) {
Modifier.testTag(containerTag)
+ .then(
+ if (config.hasNestedScrollConsumer)
+ Modifier.nestedScroll(horizontalNestedScrollConsumer)
+ else Modifier
+ )
.requiredSize(config.viewportSizePx.toDp(), itemSizePx.toDp())
}
}
@@ -265,6 +280,11 @@
Modifier.composed {
with(LocalDensity.current) {
Modifier.testTag(containerTag)
+ .then(
+ if (config.hasNestedScrollConsumer)
+ Modifier.nestedScroll(verticalNestedScrollConsumer)
+ else Modifier
+ )
.requiredSize(itemSizePx.toDp(), config.viewportSizePx.toDp())
}
}
@@ -307,4 +327,18 @@
FullyBefore(2 * itemsAround, 20, 2 * itemsAround - 1, 50),
NotInList(0, 0, 0, 0)
}
+
+ private val verticalNestedScrollConsumer =
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ return Offset(0f, available.y / 2f)
+ }
+ }
+
+ private val horizontalNestedScrollConsumer =
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ return Offset(available.x / 2f, 0f)
+ }
+ }
}
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/MoveTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/MoveTest.kt
index 5ad0300..6473c55 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/MoveTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/mouse/MoveTest.kt
@@ -25,9 +25,9 @@
import androidx.compose.ui.test.InputDispatcher
import androidx.compose.ui.test.MouseButton
import androidx.compose.ui.test.MouseInjectionScope
-import androidx.compose.ui.test.animateAlong
-import androidx.compose.ui.test.animateBy
-import androidx.compose.ui.test.animateTo
+import androidx.compose.ui.test.animateMoveAlong
+import androidx.compose.ui.test.animateMoveBy
+import androidx.compose.ui.test.animateMoveTo
import androidx.compose.ui.test.injectionscope.mouse.Common.PrimaryButton
import androidx.compose.ui.test.injectionscope.mouse.Common.runMouseInputInjectionTest
import androidx.compose.ui.test.injectionscope.mouse.Common.verifyMouseEvent
@@ -199,7 +199,7 @@
@Test
fun animatePointerTo() =
runMouseInputInjectionTest(
- mouseInput = { animateTo(position1, durationMillis = steps * T) },
+ mouseInput = { animateMoveTo(position1, durationMillis = steps * T) },
eventVerifiers =
arrayOf(
{ verifyMouseEvent(1 * T, Enter, false, distancePerStep * 1f) },
@@ -214,7 +214,7 @@
runMouseInputInjectionTest(
mouseInput = {
moveTo(position2)
- animateBy(distance, durationMillis = steps * T)
+ animateMoveBy(distance, durationMillis = steps * T)
},
eventVerifiers =
arrayOf(
@@ -229,7 +229,7 @@
@Test
fun animateAlong_fromCurrentPosition() =
runMouseInputInjectionTest(
- mouseInput = { animateAlong(curveFromHere, durationMillis = steps * T) },
+ mouseInput = { animateMoveAlong(curveFromHere, durationMillis = steps * T) },
eventVerifiers =
arrayOf(
// The curve starts at the current position (0, 0) so we expect no initial
@@ -244,7 +244,7 @@
@Test
fun animateAlong_fromOtherPosition() =
runMouseInputInjectionTest(
- mouseInput = { animateAlong(curveFromElsewhere, durationMillis = steps * T) },
+ mouseInput = { animateMoveAlong(curveFromElsewhere, durationMillis = steps * T) },
eventVerifiers =
arrayOf(
// The curve doesn't start at the current position (0, 0) so we expect an
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
index b669037..507c604 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
@@ -66,16 +66,26 @@
*
* @return The [SemanticsNodeInteraction] that is the receiver of this method
*/
+@OptIn(InternalTestApi::class)
fun SemanticsNodeInteraction.performScrollTo(): SemanticsNodeInteraction {
@OptIn(ExperimentalTestApi::class) invokeGlobalAssertions()
- @OptIn(InternalTestApi::class)
- fetchSemanticsNode("Action performScrollTo() failed.").scrollToNode(testContext.testOwner)
+ do {
+ val shouldContinueScroll =
+ fetchSemanticsNode("Action performScrollTo() failed.")
+ .scrollToNode(testContext.testOwner)
+ } while (shouldContinueScroll)
+
return this
}
-/** Implementation of [performScrollTo] */
+/**
+ * Implementation of [performScrollTo]
+ *
+ * @return True if we were able to scroll and a subsequent scroll might be needed and false if no
+ * scroll was needed.
+ */
@OptIn(InternalTestApi::class)
-private fun SemanticsNode.scrollToNode(testOwner: TestOwner) {
+private fun SemanticsNode.scrollToNode(testOwner: TestOwner): Boolean {
val scrollableNode =
findClosestParentNode { hasScrollAction().matches(it) }
?: throw AssertionError(
@@ -110,7 +120,17 @@
// And adjust for reversing properties
if (scrollableNode.isReversedVertically) dy = -dy
- testOwner.runOnUiThread { scrollableNode.config[ScrollBy].action?.invoke(dx, dy) }
+ if (
+ dx != 0f && scrollableNode.horizontalScrollAxis != null ||
+ dy != 0f && scrollableNode.verticalScrollAxis != null
+ ) {
+ // we have something to scroll
+ testOwner.runOnUiThread { scrollableNode.config[ScrollBy].action?.invoke(dx, dy) }
+ return true
+ } else {
+ // we don't have anything to scroll
+ return false // no need to scroll again
+ }
}
/**
@@ -209,12 +229,7 @@
matcher: SemanticsMatcher
): SemanticsNodeInteraction {
@OptIn(ExperimentalTestApi::class) invokeGlobalAssertions()
- var node = fetchSemanticsNode("Failed: performScrollToNode(${matcher.description})")
- matcher.findMatchInDescendants(node)?.also {
- @OptIn(InternalTestApi::class) it.scrollToNode(testContext.testOwner)
- return this
- }
-
+ val node = scrollToMatchingDescendantOrReturnScrollable(matcher) ?: return this
// If this is NOT a lazy list, but we haven't found the node above ..
if (!node.isLazyList) {
// .. throw an error that the node doesn't exist
@@ -229,30 +244,47 @@
while (true) {
// Fetch the node again
- node = fetchSemanticsNode("Failed: performScrollToNode(${matcher.description})")
- matcher.findMatchInDescendants(node)?.also {
- @OptIn(InternalTestApi::class) it.scrollToNode(testContext.testOwner)
- return this
- }
+ val newNode = scrollToMatchingDescendantOrReturnScrollable(matcher) ?: return this
// Are we there yet? Are we there yet? Are we there yet?
- if (node.horizontalScrollAxis.isAtEnd && node.verticalScrollAxis.isAtEnd) {
+ if (newNode.horizontalScrollAxis.isAtEnd && newNode.verticalScrollAxis.isAtEnd) {
// If we're finished and we haven't found the node
val msg = "No node found that matches ${matcher.description} in scrollable container"
- throw AssertionError(buildGeneralErrorMessage(msg, selector, node))
+ throw AssertionError(buildGeneralErrorMessage(msg, selector, newNode))
}
- val viewPortSize = node.layoutInfo.coordinates.boundsInParent().size
- val dx = node.horizontalScrollAxis?.let { viewPortSize.width } ?: 0f
- val dy = node.verticalScrollAxis?.let { viewPortSize.height } ?: 0f
+ val viewPortSize = newNode.layoutInfo.coordinates.boundsInParent().size
+ val dx = newNode.horizontalScrollAxis?.let { viewPortSize.width } ?: 0f
+ val dy = newNode.verticalScrollAxis?.let { viewPortSize.height } ?: 0f
// Scroll one screen
@OptIn(InternalTestApi::class)
- testContext.testOwner.runOnUiThread { node.config[ScrollBy].action?.invoke(dx, dy) }
+ testContext.testOwner.runOnUiThread { newNode.config[ScrollBy].action?.invoke(dx, dy) }
}
}
/**
+ * Searches a descendant of the caller node that matches [matcher] and scroll to that node using
+ * [scrollToNode]. Once scroll finishes this will return null. If no descendant node matches this
+ * will return the caller node.
+ */
+private fun SemanticsNodeInteraction.scrollToMatchingDescendantOrReturnScrollable(
+ matcher: SemanticsMatcher
+): SemanticsNode? {
+ var node = fetchSemanticsNode("Failed: performScrollToNode(${matcher.description})")
+ var matchedNode = matcher.scrollToMatchingDescendantOrReturnScrollable(node)
+ @OptIn(InternalTestApi::class)
+ while (matchedNode != null) {
+ val shouldContinueScroll = matchedNode.scrollToNode(testContext.testOwner)
+ if (!shouldContinueScroll) return null
+ node = fetchSemanticsNode("Failed: performScrollToNode(${matcher.description})")
+ matchedNode = matcher.scrollToMatchingDescendantOrReturnScrollable(node)
+ }
+
+ return node
+}
+
+/**
* Executes the (partial) gesture specified in the given [block]. The gesture doesn't need to be
* complete and can be resumed in a later invocation of [performGesture]. The event time is
* initialized to the current time of the [MainTestClock].
@@ -689,17 +721,19 @@
if (missingProperties.isNotEmpty()) {
val msg =
"${errorMessage()}, the node is missing [${
- missingProperties.joinToString { it.name }
- }]"
+ missingProperties.joinToString { it.name }
+ }]"
throw AssertionError(buildGeneralErrorMessage(msg, selector, node))
}
}
@Suppress("NOTHING_TO_INLINE") // Avoids doubling the stack depth for recursive search
-private inline fun SemanticsMatcher.findMatchInDescendants(root: SemanticsNode): SemanticsNode? {
+private inline fun SemanticsMatcher.scrollToMatchingDescendantOrReturnScrollable(
+ root: SemanticsNode
+): SemanticsNode? {
return root.children.firstOrNull { it.layoutInfo.isPlaced && findMatchInHierarchy(it) != null }
}
private fun SemanticsMatcher.findMatchInHierarchy(node: SemanticsNode): SemanticsNode? {
- return if (matches(node)) node else findMatchInDescendants(node)
+ return if (matches(node)) node else scrollToMatchingDescendantOrReturnScrollable(node)
}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/MouseInjectionScope.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/MouseInjectionScope.kt
index cd0593d..0b8cbf4 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/MouseInjectionScope.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/MouseInjectionScope.kt
@@ -30,7 +30,7 @@
*/
private const val SingleClickDelayMillis = 60L
-/** The default duration of mouse gestures with configurable time (e.g. [animateTo]). */
+/** The default duration of mouse gestures with configurable time (e.g. [animateMoveTo]). */
private const val DefaultMouseGestureDurationMillis: Long = 300L
/**
@@ -40,8 +40,8 @@
* individual mouse events. The individual mouse events are: [press], [moveTo] and friends,
* [release], [cancel], [scroll] and [advanceEventTime]. Full gestures are all the other functions,
* like [MouseInjectionScope.click], [MouseInjectionScope.doubleClick],
- * [MouseInjectionScope.animateTo], etc. These are built on top of the individual events and serve
- * as a good example on how you can build your own full gesture functions.
+ * [MouseInjectionScope.animateMoveTo], etc. These are built on top of the individual events and
+ * serve as a good example on how you can build your own full gesture functions.
*
* A mouse move event can be sent with [moveTo] and [moveBy]. The mouse position can be updated with
* [updatePointerTo] and [updatePointerBy], which will not send an event and only update the
@@ -405,17 +405,17 @@
*
* Example of moving the mouse along a line:
*
- * @sample androidx.compose.ui.test.samples.mouseInputAnimateTo
+ * @sample androidx.compose.ui.test.samples.mouseInputAnimateMoveTo
* @param position The position where to move the mouse to, in the node's local coordinate system
* @param durationMillis The duration of the gesture. By default 300 milliseconds.
*/
-fun MouseInjectionScope.animateTo(
+fun MouseInjectionScope.animateMoveTo(
position: Offset,
durationMillis: Long = DefaultMouseGestureDurationMillis
) {
val durationFloat = durationMillis.toFloat()
val start = currentPosition
- animateAlong(
+ animateMoveAlong(
curve = { lerp(start, position, it / durationFloat) },
durationMillis = durationMillis
)
@@ -431,11 +431,11 @@
* right and 100 pixels upwards.
* @param durationMillis The duration of the gesture. By default 300 milliseconds.
*/
-fun MouseInjectionScope.animateBy(
+fun MouseInjectionScope.animateMoveBy(
delta: Offset,
durationMillis: Long = DefaultMouseGestureDurationMillis
) {
- animateTo(currentPosition + delta, durationMillis)
+ animateMoveTo(currentPosition + delta, durationMillis)
}
/**
@@ -446,13 +446,15 @@
*
* Example of moving the mouse along a curve:
*
- * @sample androidx.compose.ui.test.samples.mouseInputAnimateAlong
+ * @sample androidx.compose.ui.test.samples.mouseInputAnimateMoveAlong
* @param curve The function that defines the position of the mouse over time for this gesture, in
- * the node's local coordinate system.
+ * the node's local coordinate system. The argument passed to the function is the time in
+ * milliseconds since the start of the animated move, and the return value is the location of the
+ * mouse at that point in time
* @param durationMillis The duration of the gesture. By default 300 milliseconds.
*/
-fun MouseInjectionScope.animateAlong(
- curve: (Long) -> Offset,
+fun MouseInjectionScope.animateMoveAlong(
+ curve: (timeMillis: Long) -> Offset,
durationMillis: Long = DefaultMouseGestureDurationMillis
) {
require(durationMillis > 0) { "Duration is 0" }
@@ -497,7 +499,7 @@
) {
updatePointerTo(start)
press(button)
- animateTo(end, durationMillis)
+ animateMoveTo(end, durationMillis)
release(button)
}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt
index 28b4a04..44f8345 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt
@@ -456,13 +456,15 @@
* coordinates are in the node's local coordinate system, where (0, 0) is the top left corner of the
* node. The default duration is 200 milliseconds.
*
- * @param curve The function that defines the position of the gesture over time
+ * @param curve The function that describes the gesture. The argument passed to the function is the
+ * time in milliseconds since the start of the swipe, and the return value is the location of the
+ * pointer at that point in time.
* @param durationMillis The duration of the gesture
* @param keyTimes An optional list of timestamps in milliseconds at which a move event must be
* sampled
*/
fun TouchInjectionScope.swipe(
- curve: (Long) -> Offset,
+ curve: (timeMillis: Long) -> Offset,
durationMillis: Long = 200,
keyTimes: List<Long> = emptyList()
) {
@@ -478,13 +480,16 @@
* coordinates are in the node's local coordinate system, where (0, 0) is the top left corner of the
* node. The default duration is 200 milliseconds.
*
- * @param curves The functions that define the position of the gesture over time
+ * @param curves The functions that describe the gesture. Function _i_ defines the position over
+ * time for pointer id _i_. The argument passed to each function is the time in milliseconds since
+ * the start of the swipe, and the return value is the location of that pointer at that point in
+ * time.
* @param durationMillis The duration of the gesture
* @param keyTimes An optional list of timestamps in milliseconds at which a move event must be
* sampled
*/
fun TouchInjectionScope.multiTouchSwipe(
- curves: List<(Long) -> Offset>,
+ curves: List<(timeMillis: Long) -> Offset>,
durationMillis: Long = 200,
keyTimes: List<Long> = emptyList()
) {
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/Html.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/Html.android.kt
index b46b704..3a81e24 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/Html.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/Html.android.kt
@@ -50,10 +50,39 @@
import org.xml.sax.ContentHandler
import org.xml.sax.XMLReader
-actual fun AnnotatedString.Companion.fromHtml(
+/**
+ * Converts a string with HTML tags into [AnnotatedString].
+ *
+ * If you define your string in the resources, make sure to use HTML-escaped opening brackets "<"
+ * instead of "<".
+ *
+ * For a list of supported tags go check
+ * [Styling with HTML markup](https://developer.android.com/guide/topics/resources/string-resource#StylingWithHTML)
+ * guide. Note that bullet lists are not **yet** available.
+ *
+ * @param htmlString HTML-tagged string to be parsed to construct AnnotatedString
+ * @param linkStyles style configuration to be applied to links present in the string in different
+ * styles
+ * @param linkInteractionListener a listener that will be attached to links that are present in the
+ * string and triggered when user clicks on those links. When set to null, which is a default, the
+ * system will try to open the corresponding links with the
+ * [androidx.compose.ui.platform.UriHandler] composition local
+ *
+ * Note that any link style passed directly to this method will be merged with the styles set
+ * directly on a HTML-tagged string. For example, if you set a color of the link via the span
+ * annotation to "red" but also pass a green color via the [linkStyles], the link will be displayed
+ * as green. If, however, you pass a green background via the [linkStyles] instead, the link will be
+ * displayed as red on a green background.
+ *
+ * Example of displaying styled string from resources
+ *
+ * @sample androidx.compose.ui.text.samples.AnnotatedStringFromHtml
+ * @see LinkAnnotation
+ */
+fun AnnotatedString.Companion.fromHtml(
htmlString: String,
- linkStyles: TextLinkStyles?,
- linkInteractionListener: LinkInteractionListener?
+ linkStyles: TextLinkStyles? = null,
+ linkInteractionListener: LinkInteractionListener? = null
): AnnotatedString {
// Check ContentHandlerReplacementTag kdoc for more details
val stringToParse = "<$ContentHandlerReplacementTag />$htmlString"
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Html.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Html.kt
deleted file mode 100644
index 53ed97a4..0000000
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Html.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.text
-
-/**
- * Converts a string with HTML tags into [AnnotatedString].
- *
- * If you define your string in the resources, make sure to use HTML-escaped opening brackets "<"
- * instead of "<".
- *
- * For a list of supported tags go check
- * [Styling with HTML markup](https://developer.android.com/guide/topics/resources/string-resource#StylingWithHTML)
- * guide. Note that bullet lists are not **yet** available.
- *
- * @param htmlString HTML-tagged string to be parsed to construct AnnotatedString
- * @param linkStyles style configuration to be applied to links present in the string in different
- * styles
- * @param linkInteractionListener a listener that will be attached to links that are present in the
- * string and triggered when user clicks on those links. When set to null, which is a default, the
- * system will try to open the corresponding links with the
- * [androidx.compose.ui.platform.UriHandler] composition local
- *
- * Note that any link style passed directly to this method will be merged with the styles set
- * directly on a HTML-tagged string. For example, if you set a color of the link via the span
- * annotation to "red" but also pass a green color via the [linkStyles], the link will be displayed
- * as green. If, however, you pass a green background via the [linkStyles] instead, the link will be
- * displayed as red on a green background.
- *
- * Example of displaying styled string from resources
- *
- * @sample androidx.compose.ui.text.samples.AnnotatedStringFromHtml
- * @see LinkAnnotation
- */
-expect fun AnnotatedString.Companion.fromHtml(
- htmlString: String,
- linkStyles: TextLinkStyles? = null,
- linkInteractionListener: LinkInteractionListener? = null
-): AnnotatedString
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 291f1f2..9284277 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1932,6 +1932,10 @@
property public final long uptimeMillis;
}
+ public fun interface PointerInputEventHandler {
+ method public suspend operator Object? invoke(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
public abstract class PointerInputFilter {
ctor public PointerInputFilter();
method public boolean getInterceptOutOfBoundsChildEvents();
@@ -1994,18 +1998,25 @@
}
public final class SuspendingPointerInputFilterKt {
- method public static androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode SuspendingPointerInputModifierNode(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> pointerInputHandler);
- method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
- method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
- method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object?[] keys, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
+ method public static androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode SuspendingPointerInputModifierNode(androidx.compose.ui.input.pointer.PointerInputEventHandler pointerInputEventHandler);
+ method @Deprecated public static androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode SuspendingPointerInputModifierNode(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> pointerInputHandler);
+ method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, androidx.compose.ui.input.pointer.PointerInputEventHandler block);
+ method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, Object? key2, androidx.compose.ui.input.pointer.PointerInputEventHandler block);
+ method @Deprecated public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
+ method @Deprecated public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
+ method @Deprecated public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, java.lang.Object?... keys, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
+ method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object?[] keys, androidx.compose.ui.input.pointer.PointerInputEventHandler block);
method @Deprecated public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
}
public sealed interface SuspendingPointerInputModifierNode extends androidx.compose.ui.node.PointerInputModifierNode {
- method public kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> getPointerInputHandler();
+ method public default androidx.compose.ui.input.pointer.PointerInputEventHandler getPointerInputEventHandler();
+ method @Deprecated public kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> getPointerInputHandler();
method public void resetPointerInputHandler();
- method public void setPointerInputHandler(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?>);
- property public abstract kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> pointerInputHandler;
+ method public default void setPointerInputEventHandler(androidx.compose.ui.input.pointer.PointerInputEventHandler);
+ method @Deprecated public void setPointerInputHandler(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?>);
+ property public default androidx.compose.ui.input.pointer.PointerInputEventHandler pointerInputEventHandler;
+ property @Deprecated public abstract kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> pointerInputHandler;
}
}
@@ -3310,6 +3321,7 @@
method public int getCheckbox();
method public int getDropdownList();
method public int getImage();
+ method public int getNumberPicker();
method public int getRadioButton();
method public int getSwitch();
method public int getTab();
@@ -3317,6 +3329,7 @@
property public final int Checkbox;
property public final int DropdownList;
property public final int Image;
+ property public final int NumberPicker;
property public final int RadioButton;
property public final int Switch;
property public final int Tab;
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index d3a946f..56a0613 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1932,6 +1932,10 @@
property public final long uptimeMillis;
}
+ public fun interface PointerInputEventHandler {
+ method public suspend operator Object? invoke(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
public abstract class PointerInputFilter {
ctor public PointerInputFilter();
method public boolean getInterceptOutOfBoundsChildEvents();
@@ -1994,18 +1998,25 @@
}
public final class SuspendingPointerInputFilterKt {
- method public static androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode SuspendingPointerInputModifierNode(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> pointerInputHandler);
- method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
- method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
- method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object?[] keys, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
+ method public static androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode SuspendingPointerInputModifierNode(androidx.compose.ui.input.pointer.PointerInputEventHandler pointerInputEventHandler);
+ method @Deprecated public static androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode SuspendingPointerInputModifierNode(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> pointerInputHandler);
+ method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, androidx.compose.ui.input.pointer.PointerInputEventHandler block);
+ method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, Object? key2, androidx.compose.ui.input.pointer.PointerInputEventHandler block);
+ method @Deprecated public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
+ method @Deprecated public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
+ method @Deprecated public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, java.lang.Object?... keys, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
+ method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, Object?[] keys, androidx.compose.ui.input.pointer.PointerInputEventHandler block);
method @Deprecated public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
}
public sealed interface SuspendingPointerInputModifierNode extends androidx.compose.ui.node.PointerInputModifierNode {
- method public kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> getPointerInputHandler();
+ method public default androidx.compose.ui.input.pointer.PointerInputEventHandler getPointerInputEventHandler();
+ method @Deprecated public kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> getPointerInputHandler();
method public void resetPointerInputHandler();
- method public void setPointerInputHandler(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?>);
- property public abstract kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> pointerInputHandler;
+ method public default void setPointerInputEventHandler(androidx.compose.ui.input.pointer.PointerInputEventHandler);
+ method @Deprecated public void setPointerInputHandler(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?>);
+ property public default androidx.compose.ui.input.pointer.PointerInputEventHandler pointerInputEventHandler;
+ property @Deprecated public abstract kotlin.jvm.functions.Function2<androidx.compose.ui.input.pointer.PointerInputScope,kotlin.coroutines.Continuation<? super kotlin.Unit>,java.lang.Object?> pointerInputHandler;
}
}
@@ -3370,6 +3381,7 @@
method public int getCheckbox();
method public int getDropdownList();
method public int getImage();
+ method public int getNumberPicker();
method public int getRadioButton();
method public int getSwitch();
method public int getTab();
@@ -3377,6 +3389,7 @@
property public final int Checkbox;
property public final int DropdownList;
property public final int Image;
+ property public final int NumberPicker;
property public final int RadioButton;
property public final int Switch;
property public final int Tab;
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 70d00ee..d2d517a 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -94,7 +94,7 @@
implementation('androidx.collection:collection:1.0.0')
implementation("androidx.customview:customview-poolingcontainer:1.0.0")
implementation("androidx.savedstate:savedstate-ktx:1.2.1")
- api("androidx.lifecycle:lifecycle-runtime-compose:2.8.2")
+ api("androidx.lifecycle:lifecycle-runtime-compose:2.8.3")
implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
implementation("androidx.emoji2:emoji2:1.2.0")
@@ -104,13 +104,13 @@
// converting `lifecycle-runtime-compose` to KMP triggered a Gradle bug. Adding
// the `livedata` dependency directly works around the issue.
// See https://github.com/gradle/gradle/issues/14220 for details.
- compileOnly("androidx.lifecycle:lifecycle-livedata-core:2.8.2")
+ compileOnly("androidx.lifecycle:lifecycle-livedata-core:2.8.3")
// `compose-ui` has a transitive dependency on `lifecycle-viewmodel-savedstate`, and
// converting `lifecycle-runtime-compose` to KMP triggered a Gradle bug. Adding
// the `lifecycle-viewmodel-savedstate` dependency directly works around the issue.
// See https://github.com/gradle/gradle/issues/14220 for details.
- compileOnly("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2")
+ compileOnly("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.3")
}
}
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index a2d8987..7ade151 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -66,6 +66,8 @@
import androidx.compose.ui.demos.gestures.NestedScrollDispatchDemo
import androidx.compose.ui.demos.gestures.NestedScrollingDemo
import androidx.compose.ui.demos.gestures.PointerInputDuringSubComp
+import androidx.compose.ui.demos.gestures.PointerInputLambdaExecutions
+import androidx.compose.ui.demos.gestures.PointerInputLambdaExecutionsUsingExternalFunctions
import androidx.compose.ui.demos.gestures.PopupDragDemo
import androidx.compose.ui.demos.gestures.PressIndicatorGestureFilterDemo
import androidx.compose.ui.demos.gestures.RawDragGestureFilterDemo
@@ -143,7 +145,11 @@
ComposableDemo("Popup Drag") { PopupDragDemo() },
ComposableDemo("Double Tap in Tap") { DoubleTapInTapDemo() },
ComposableDemo("Nested Long Press") { NestedLongPressDemo() },
- ComposableDemo("Pointer Input During Sub Comp") { PointerInputDuringSubComp() }
+ ComposableDemo("Pointer Input During Sub Comp") { PointerInputDuringSubComp() },
+ ComposableDemo("Pointer Input Lambda Stats") { PointerInputLambdaExecutions() },
+ ComposableDemo("Pointer Input Lambda Stats w/ external function") {
+ PointerInputLambdaExecutionsUsingExternalFunctions()
+ }
)
),
DemoCategory(
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/PointerInputLambdaExecutions.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/PointerInputLambdaExecutions.kt
new file mode 100644
index 0000000..025e442
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/PointerInputLambdaExecutions.kt
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.demos.gestures
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.PointerInputEventHandler
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+
+/** Shows how underlying lambdas are executed. */
+@Composable
+fun PointerInputLambdaExecutions() {
+
+ var topBoxText by remember { mutableStateOf("Click button to see details") }
+ var firstPointerInputLambdaExecutionCount by remember { mutableIntStateOf(0) }
+ var firstPointerInputPressCounter by remember { mutableIntStateOf(0) }
+ var firstPointerInputMoveCounter by remember { mutableIntStateOf(0) }
+ var firstPointerInputReleaseCounter by remember { mutableIntStateOf(0) }
+
+ var bottomBoxText by remember { mutableStateOf("Click button to see details") }
+ var secondPointerInputLambdaExecutionCount by remember { mutableIntStateOf(0) }
+ var secondPointerInputPressCounter by remember { mutableIntStateOf(0) }
+ var secondPointerInputMoveCounter by remember { mutableIntStateOf(0) }
+ var secondPointerInputReleaseCounter by remember { mutableIntStateOf(0) }
+
+ Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
+ Text(
+ modifier = Modifier.fillMaxWidth().weight(1f),
+ textAlign = TextAlign.Center,
+ text = "See how underlying lambdas are executed"
+ )
+
+ Column(modifier = Modifier.fillMaxSize().background(Color.LightGray).padding(8.dp)) {
+ Text(
+ modifier =
+ Modifier.size(400.dp).background(Color.Green).weight(1f).pointerInput(Unit) {
+ firstPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Press -> {
+ firstPointerInputPressCounter++
+ }
+ PointerEventType.Move -> {
+ firstPointerInputMoveCounter++
+ }
+ PointerEventType.Release -> {
+ firstPointerInputReleaseCounter++
+ }
+ }
+ topBoxText =
+ "Lambda Execution: $firstPointerInputLambdaExecutionCount,\n" +
+ "Press: $firstPointerInputPressCounter,\n" +
+ "Move: $firstPointerInputMoveCounter,\n" +
+ "Release: $firstPointerInputReleaseCounter"
+ }
+ }
+ },
+ textAlign = TextAlign.Center,
+ text = topBoxText
+ )
+
+ Text(
+ modifier =
+ Modifier.size(400.dp).background(Color.Red).weight(1f).pointerInput(Unit) {
+ secondPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Press -> {
+ secondPointerInputPressCounter++
+ }
+ PointerEventType.Move -> {
+ secondPointerInputMoveCounter++
+ }
+ PointerEventType.Release -> {
+ secondPointerInputReleaseCounter++
+ }
+ }
+ bottomBoxText =
+ "Lambda Execution: $secondPointerInputLambdaExecutionCount,\n" +
+ "Press: $secondPointerInputPressCounter,\n" +
+ "Move: $secondPointerInputMoveCounter,\n" +
+ "Release: $secondPointerInputReleaseCounter"
+ }
+ }
+ },
+ textAlign = TextAlign.Center,
+ text = bottomBoxText
+ )
+ }
+ }
+}
+
+/**
+ * Shows how underlying lambdas are executed but instead of passing the lambdas directly, we call a
+ * separate external function to return the PointerInputEventHandler.
+ */
+@Composable
+fun PointerInputLambdaExecutionsUsingExternalFunctions() {
+
+ var topBoxText by remember { mutableStateOf("Click button to see details") }
+ var firstPointerInputLambdaExecutionCount by remember { mutableIntStateOf(0) }
+ var firstPointerInputPressCounter by remember { mutableIntStateOf(0) }
+ var firstPointerInputMoveCounter by remember { mutableIntStateOf(0) }
+ var firstPointerInputReleaseCounter by remember { mutableIntStateOf(0) }
+
+ var bottomBoxText by remember { mutableStateOf("Click button to see details") }
+ var secondPointerInputLambdaExecutionCount by remember { mutableIntStateOf(0) }
+ var secondPointerInputPressCounter by remember { mutableIntStateOf(0) }
+ var secondPointerInputMoveCounter by remember { mutableIntStateOf(0) }
+ var secondPointerInputReleaseCounter by remember { mutableIntStateOf(0) }
+
+ Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
+ Text(
+ modifier = Modifier.fillMaxWidth().weight(1f),
+ textAlign = TextAlign.Center,
+ text = "See how underlying lambdas are executed"
+ )
+
+ Column(modifier = Modifier.fillMaxSize().background(Color.LightGray).padding(8.dp)) {
+ Text(
+ modifier =
+ Modifier.size(400.dp)
+ .background(Color.Green)
+ .weight(1f)
+ .pointerInput(
+ Unit,
+ createPointerInputEventHandlerReturnTypeInterface {
+ firstPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Press -> {
+ firstPointerInputPressCounter++
+ }
+ PointerEventType.Move -> {
+ firstPointerInputMoveCounter++
+ }
+ PointerEventType.Release -> {
+ firstPointerInputReleaseCounter++
+ }
+ }
+ topBoxText =
+ "Lambda Execution: $firstPointerInputLambdaExecutionCount,\n" +
+ "Press: $firstPointerInputPressCounter,\n" +
+ "Move: $firstPointerInputMoveCounter,\n" +
+ "Release: $firstPointerInputReleaseCounter"
+ }
+ }
+ }
+ ),
+ textAlign = TextAlign.Center,
+ text = topBoxText
+ )
+
+ Text(
+ modifier =
+ Modifier.size(400.dp)
+ .background(Color.Red)
+ .weight(1f)
+ .pointerInput(
+ Unit,
+ createPointerInputEventHandlerReturnTypeInterface {
+ secondPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Press -> {
+ secondPointerInputPressCounter++
+ }
+ PointerEventType.Move -> {
+ secondPointerInputMoveCounter++
+ }
+ PointerEventType.Release -> {
+ secondPointerInputReleaseCounter++
+ }
+ }
+ bottomBoxText =
+ "Lambda Execution: $secondPointerInputLambdaExecutionCount,\n" +
+ "Press: $secondPointerInputPressCounter,\n" +
+ "Move: $secondPointerInputMoveCounter,\n" +
+ "Release: $secondPointerInputReleaseCounter"
+ }
+ }
+ }
+ ),
+ textAlign = TextAlign.Center,
+ text = bottomBoxText
+ )
+ }
+ }
+}
+
+// The return type (PointerInputEventHandler) is actually an interface, so Kotlin will create class
+// type based on calling location/order.
+private fun createPointerInputEventHandlerReturnTypeInterface(
+ lambda: PointerInputEventHandler
+): PointerInputEventHandler {
+ return lambda
+}
diff --git a/compose/ui/ui/proguard-rules.pro b/compose/ui/ui/proguard-rules.pro
index fd62154..29f2984 100644
--- a/compose/ui/ui/proguard-rules.pro
+++ b/compose/ui/ui/proguard-rules.pro
@@ -40,3 +40,10 @@
# For methods returning Nothing
static java.lang.Void throw*Exception(...);
}
+
+# When pointer input modifier nodes are added dynamically and have the same keys (common when
+# developers `Unit` for their keys), we need a way to differentiate them and using a
+# functional interface and comparing classes allows us to do that.
+-keepnames class androidx.compose.ui.input.pointer.PointerInputEventHandler {
+ *;
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index f14d476..7a2f9bc95 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -565,6 +565,21 @@
}
@Test
+ fun testCreateAccessibilityNodeInfo_numberPicker_expectedClassName() {
+ // Arrange.
+ setContent { Box(Modifier.semantics { role = Role.NumberPicker }.testTag(tag)) }
+ val virtualId = rule.onNodeWithTag(tag).semanticsId
+
+ // Act.
+ val info = rule.runOnIdle { createAccessibilityNodeInfo(virtualId) }
+
+ // Assert.
+ rule.runOnIdle {
+ with(info) { assertThat(className).isEqualTo("android.widget.NumberPicker") }
+ }
+ }
+
+ @Test
fun testCreateAccessibilityNodeInfo_progressIndicator_determinate() {
// Arrange.
setContent { Box(Modifier.progressSemantics(0.5f).testTag(tag)) { BasicText("Text") } }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAssistTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAssistTest.kt
index 2d08cc5..7420f15 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAssistTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAssistTest.kt
@@ -40,11 +40,11 @@
private val contentTag = "content_tag"
private val accessibilityClassName = "android.view.ViewGroup"
- // Test that the assistStructure only has its classname set on API levels 26 and 27. See
- // b/251152083 for more information.
+ // Test that the assistStructure only has its classname set on API levels 23 to 27. See
+ // b/251152083 and b/320768586 for more information.
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = 26, maxSdkVersion = 27)
+ @SdkSuppress(minSdkVersion = 23, maxSdkVersion = 27)
fun verifyAssistStructureSet() {
val viewStructure: ViewStructure = FakeViewStructure()
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
index 0a70dd6..73b2b54 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
@@ -46,7 +46,7 @@
* @param autofillHints The autofill hint. If this value not specified, we use heuristics to
* determine what data to use while performing autofill.
*/
-@RequiresApi(Build.VERSION_CODES.O)
+@RequiresApi(Build.VERSION_CODES.M)
internal data class FakeViewStructure(
var virtualId: Int = 0,
var packageName: String? = null,
@@ -54,7 +54,8 @@
var entryName: String? = null,
var children: MutableList<FakeViewStructure> = mutableListOf(),
var bounds: Rect? = null,
- private val autofillId: AutofillId? = generateAutofillId(),
+ private val autofillId: AutofillId? =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) generateAutofillId() else null,
internal var autofillType: Int = View.AUTOFILL_TYPE_NONE,
internal var autofillHints: Array<out String> = arrayOf()
) : ViewStructure() {
@@ -92,6 +93,9 @@
@GuardedBy("this") private var previousId = 0
private val NO_SESSION = 0
+ // Android API level 26 introduced Autofill. Prior to API level 26, no autofill ID will be
+ // provided.
+ @RequiresApi(Build.VERSION_CODES.O)
@Synchronized
private fun generateAutofillId(): AutofillId {
var autofillId: AutofillId? = null
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
index 39e6b37..c36184a 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
@@ -176,9 +176,13 @@
rule.onNodeWithTag(tag).captureToImage().assertPixels { Color.Blue }
- rule.runOnIdle {
- // we also make sure we didn't have to re-record to display the content
- assertThat(recordCalls).isEqualTo(1)
+ // See b/350074295 on View backed layers on API level 28, the record method is invoked
+ // an additional time.
+ if (Build.VERSION.SDK_INT != Build.VERSION_CODES.P) {
+ rule.runOnIdle {
+ // we also make sure we didn't have to re-record to display the content
+ assertThat(recordCalls).isEqualTo(1)
+ }
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
index 99bdb0d..fa6a985 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
@@ -2693,25 +2693,8 @@
}
/*
- * Tests TOUCH events are triggered incorrectly when dynamically adding a pointer input modifier
- * (which uses Unit for its key [bad]) ABOVE an existing pointer input modifier. This is more
- * of an "education test" for developers to see how things can go wrong if you use "Unit" for
- * your key in pointer input and pointer input modifiers are later added dynamically.
- *
- * Note: Even though we are dynamically adding a new pointer input modifier above the existing
- * pointer input modifier, Compose actually reuses the existing pointer input modifier to
- * contain the new pointer input modifier. It then adds a new pointer input modifier below that
- * one and copies in the original (non-dynamic) pointer input modifier into that. However, in
- * this case, because we are using the "Unit" for both keys, Compose thinks they are the same
- * pointer input modifier, so it never replaces the existing lambda with the dynamic pointer
- * input modifier node's lambda. This is why you should not use Unit for your key.
- *
- * Why can't the lambdas passed into pointer input be compared? We can't memoize them because
- * they are outside of a Compose scope (defined in a Modifier extension function), so
- * developers need to pass a unique key(s) as a way to let us know when to update the lambda.
- * You can do that with a unique key for each pointer input modifier and/or take it a step
- * further and use captured values in the lambda as keys (ones that change lambda
- * behavior).
+ * Tests TOUCH events are triggered correctly when dynamically adding a pointer input modifier
+ * (which uses Unit for its key) ABOVE an existing pointer input modifier.
*
* Specific events:
* 1. UI Element (modifier 1 only): PRESS (touch)
@@ -2723,7 +2706,7 @@
* 7. UI Element (modifier 1 and 2): RELEASE (touch)
*/
@Test
- fun dynamicInputModifierWithUnitKey_addsAboveExistingModifier_failsToTriggerNewModifier() {
+ fun dynamicInputModifierWithUnitKey_addsAboveExistingModifier_triggersBothModifiers() {
// --> Arrange
var box1LayoutCoordinates: LayoutCoordinates? = null
@@ -2907,20 +2890,16 @@
// executed again to allow devs to reset their gesture detectors for the new Modifier
// chain changes.
assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
- // The dynamic one has been added, so we execute its thing as well.
- assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+ // The dynamic one has been added, so we execute its lambda as well.
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
// Verify Box 1 existing modifier events
- // This is 2 because the dynamic modifier added before the existing one, is using Unit
- // for the key, so the comparison shows that it doesn't need to update the lambda...
- // Thus, it uses the old lambda (why it is very important you don't use Unit for your
- // key.
- assertThat(preexistingModifierPress).isEqualTo(3)
+ assertThat(preexistingModifierPress).isEqualTo(2)
assertThat(preexistingModifierMove).isEqualTo(1)
assertThat(preexistingModifierRelease).isEqualTo(1)
// Verify Box 1 dynamically added modifier events
- assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierPress).isEqualTo(1)
assertThat(dynamicModifierMove).isEqualTo(0)
assertThat(dynamicModifierRelease).isEqualTo(0)
@@ -2936,16 +2915,16 @@
)
rule.runOnUiThread {
assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
- assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
// Verify Box 1 existing modifier events
- assertThat(preexistingModifierPress).isEqualTo(3)
- assertThat(preexistingModifierMove).isEqualTo(3)
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(2)
assertThat(preexistingModifierRelease).isEqualTo(1)
// Verify Box 1 dynamically added modifier events
- assertThat(dynamicModifierPress).isEqualTo(0)
- assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(1)
assertThat(dynamicModifierRelease).isEqualTo(0)
assertThat(pointerEvent).isNotNull()
@@ -2960,17 +2939,17 @@
)
rule.runOnUiThread {
assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
- assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
// Verify Box 1 existing modifier events
- assertThat(preexistingModifierPress).isEqualTo(3)
- assertThat(preexistingModifierMove).isEqualTo(3)
- assertThat(preexistingModifierRelease).isEqualTo(3)
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(2)
+ assertThat(preexistingModifierRelease).isEqualTo(2)
// Verify Box 1 dynamically added modifier events
- assertThat(dynamicModifierPress).isEqualTo(0)
- assertThat(dynamicModifierMove).isEqualTo(0)
- assertThat(dynamicModifierRelease).isEqualTo(0)
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(1)
+ assertThat(dynamicModifierRelease).isEqualTo(1)
assertThat(pointerEvent).isNotNull()
assertThat(eventsThatShouldNotTrigger).isFalse()
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
index 2a6af69..be8afd9 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
@@ -20,9 +20,11 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.ReusableContent
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.testutils.assertModifierIsPure
@@ -729,9 +731,9 @@
isDebugInspectorInfoEnabled = true
rule.setContent {
- val pointerInputHandler: suspend PointerInputScope.() -> Unit = {}
+ val pointerInputEventHandler = PointerInputEventHandler {}
val modifier =
- Modifier.pointerInput(Unit, pointerInputHandler) as SuspendPointerInputElement
+ Modifier.pointerInput(Unit, pointerInputEventHandler) as SuspendPointerInputElement
assertThat(modifier.nameFallback).isEqualTo("pointerInput")
assertThat(modifier.valueOverride).isNull()
@@ -740,7 +742,7 @@
ValueElement("key1", Unit),
ValueElement("key2", null),
ValueElement("keys", null),
- ValueElement("pointerInputHandler", pointerInputHandler)
+ ValueElement("pointerInputEventHandler", pointerInputEventHandler)
)
}
}
@@ -748,7 +750,7 @@
@Test
@SmallTest
fun testEquality_key() {
- val block: suspend PointerInputScope.() -> Unit = {}
+ val block = PointerInputEventHandler {}
assertModifierIsPure { toggleInput -> Modifier.pointerInput(toggleInput, block = block) }
}
@@ -756,8 +758,8 @@
@Test
@SmallTest
fun testEquality_block() {
- val block1: suspend PointerInputScope.() -> Unit = {}
- val block2: suspend PointerInputScope.() -> Unit = {}
+ val block1 = PointerInputEventHandler {}
+ val block2 = PointerInputEventHandler {}
assertModifierIsPure { toggleInput ->
val block = if (toggleInput) block1 else block2
@@ -1154,19 +1156,150 @@
rule.runOnIdle { assertThat(cancelled).isTrue() }
}
+ // Tests cases where the key does not change (`Unit` in this case), but the lambda (the `block`
+ // parameter) does change. Thus, the previous lambda/block is cancelled (what developers
+ // expect). For tests that bypass this behavior, see
+ // [changePointerInputBlockGeneratedViaExternalFunctionInside_blockNotCancelled].
@Test
@MediumTest
- fun testUpdatingBlockDoesNotRestartPointerInput() {
+ fun testChangePointerInputBlockCancelsPreviousPointerInputBlock() {
val tag = "box"
var cancelled = false
- val lambda1: suspend PointerInputScope.() -> Unit = {
+ // PointerInputEventHandler is an interface, so the class produced is unique to this
+ // call site.
+ val lambda1 = PointerInputEventHandler {
try {
suspendCancellableCoroutine<Unit> {}
} catch (e: CancellationException) {
cancelled = true
}
}
- val lambda2: suspend PointerInputScope.() -> Unit = {}
+ // Same as above, this class will be unique/different from lambda1.
+ val lambda2 = PointerInputEventHandler {}
+ var block by mutableStateOf(lambda1)
+
+ rule.setContent {
+ Box(
+ Modifier.testTag(tag)
+ .fillMaxSize()
+ // Because we are using `Unit` for the key (and it doesn't change), the
+ // class is used as the comparison to cancel and restart the block.
+ .pointerInput(key1 = Unit, key2 = Unit, block = block)
+ )
+ }
+
+ rule.onNodeWithTag(tag).performClick()
+ rule.runOnIdle { assertThat(cancelled).isFalse() }
+
+ block = lambda2
+ // Because the lambda has changed, the previous block is cancelled.
+ rule.runOnIdle { assertThat(cancelled).isTrue() }
+ }
+
+ // This test is similar to the test above
+ // (`testChangePointerInputBlockCancelsPreviousPointerInputBlock()`), but it uses a custom
+ // function to create the new instances of the functional interfaces (PointerInputEventHandler)
+ // vs. creating them directly. The results are the same, because the custom function takes an
+ // argument of functional interface (meaning a new class is created based on test's call site,
+ // not from a site within the custom function). In the end, it behaves as if we are just
+ // creating an instance of the functional interface directly in the test.
+ @Test
+ @MediumTest
+ fun changePointerInputBlockGeneratedViaExternalFunctionParameter_cancelsPreviousBlock() {
+ val tag = "box"
+ var cancelled = false
+
+ // createPointerInputEventHandlerReturnTypeInterface() takes a PointerInputEventHandler
+ // parameter, so the new class will be created at this call site (unique to lambda2).
+ val lambda1 = createPointerInputEventHandlerReturnTypeInterface {
+ try {
+ suspendCancellableCoroutine<Unit> {}
+ } catch (e: CancellationException) {
+ cancelled = true
+ }
+ }
+ val lambda2 = createPointerInputEventHandlerReturnTypeInterface {}
+ var block by mutableStateOf(lambda1)
+
+ rule.setContent {
+ Box(Modifier.testTag(tag).fillMaxSize().pointerInput(key1 = Unit, block = block))
+ }
+
+ rule.onNodeWithTag(tag).performClick()
+ rule.runOnIdle { assertThat(cancelled).isFalse() }
+
+ block = lambda2
+ rule.runOnIdle { assertThat(cancelled).isTrue() }
+ }
+
+ // The next three tests cover the somewhat rare case of circumventing the behavior of
+ // `.pointerInput()` when `Unit` is used for key(s) and the `block` parameter changes (current
+ // behavior **cancels** the previous `block` and the new `block` executes when a new event
+ // arrives). Circumventing this behavior means the previous `block` won't cancel.
+ //
+ // These tests are to verify the behavior does not change. We do not recommend this approach to
+ // developers (it probably does not make sense to avoid cancelling the previous `block`).
+ //
+ // To bypass the current behavior, each `block` needs to be created from the same class, since
+ // we determine if a block has changed by class type. (Usually, a new class will be created
+ // for each call site when you pass in a trailing lambda to `.pointerInput()`.)
+ //
+ // These tests change the call sites from unique locations to the same location for all `block`
+ // creation (see tests for details).
+
+ // Test 1 of creating the same class type for all `block` parameters.
+ // The block instances are created inside a separate function
+ // ([createPointerInputEventHandlerWithSameClassEverytime()]) and returned to this test (all
+ // instances are of the same class). Since we are not using keys (just `Unit`) and the
+ // classes aren't different, the block will not be cancelled (even though it's changed). See
+ // above for more details.
+ @Test
+ @MediumTest
+ fun changePointerInputBlockGeneratedViaExternalFunctionInside_blockNotCancelled() {
+ val tag = "box"
+ var cancelled = false
+
+ val lambda1 = createPointerInputEventHandlerWithSameClassEverytime {
+ try {
+ suspendCancellableCoroutine<Unit> {}
+ } catch (e: CancellationException) {
+ cancelled = true
+ }
+ }
+ // Both lambda1 and lambda2 will be of the same class type (see fun for details).
+ val lambda2 = createPointerInputEventHandlerWithSameClassEverytime {}
+ var block by mutableStateOf(lambda1)
+
+ rule.setContent {
+ Box(Modifier.testTag(tag).fillMaxSize().pointerInput(key1 = Unit, block = block))
+ }
+
+ rule.onNodeWithTag(tag).performClick()
+ rule.runOnIdle { assertThat(cancelled).isFalse() }
+
+ block = lambda2
+ rule.runOnIdle { assertThat(cancelled).isFalse() }
+ }
+
+ // Test 2 of creating the same class type for all `block` parameters.
+ // The block instances are created all at once inside a separate function
+ // ([createPointerInputHandlersThatCaptureWithCompose()]) and returned to this test (all
+ // instances are of the same class). That function also captures values.
+ // Since we are not using keys (just `Unit`) and the classes aren't different so the block will
+ // not be cancelled (even though it's changed). See above for more details.
+ @Test
+ @MediumTest
+ fun multipleClassesCreatedFromFunInterfaceInSeparateFunctionWithKotlinCapture_classesMatch() {
+ val tag = "box"
+ var cancelled = false
+
+ val (lambda1, lambda2) = createPointerInputHandlersThatCapture { cancelled = true }
+ val (lambda1Copy, lambda2Copy) = createPointerInputHandlersThatCapture { cancelled = true }
+
+ // Classes
+ assertThat(lambda1::class).isEqualTo(lambda1Copy::class)
+ assertThat(lambda2::class).isEqualTo(lambda2Copy::class)
+
var block by mutableStateOf(lambda1)
rule.setContent {
@@ -1181,9 +1314,99 @@
rule.runOnIdle { assertThat(cancelled).isFalse() }
+ // The keys have not changed, BUT the class are different between lambda1 and lambda2, so
+ // it will trigger a cancellation of lambda1.
block = lambda2
+ rule.runOnIdle { assertThat(cancelled).isTrue() }
+
+ // Reset
+ block = lambda1
+ cancelled = false
+
+ rule.onNodeWithTag(tag).performClick()
+
rule.runOnIdle { assertThat(cancelled).isFalse() }
+
+ // The keys have not changed AND the class between lambda1 and lambda1Copy are the same, so
+ // it will NOT trigger a cancellation.
+ block = lambda1Copy
+ rule.onNodeWithTag(tag).performClick()
+
+ rule.runOnIdle { assertThat(cancelled).isFalse() }
+ }
+
+ // Test 3 of creating the same class type for all `block` parameters.
+ // Same as test 2 above
+ // ([changePointerInputBlockGeneratedViaExternalFunctionInsideMultiple_blockNotCancelled()])
+ // but uses Compose for capture vs. standard Kotlin.
+ @Test
+ @MediumTest
+ fun multipleClassesCreatedFromFunInterfaceInSeparateFunctionWithComposeCapture_classesMatch() {
+ val tag = "box"
+ val cancelled = mutableStateOf(false)
+
+ lateinit var lambda1: PointerInputEventHandler
+ lateinit var lambda2: PointerInputEventHandler
+ lateinit var lambda1Copy: PointerInputEventHandler
+ lateinit var lambda2Copy: PointerInputEventHandler
+
+ // Initialized to empty block
+ var block: PointerInputEventHandler by mutableStateOf(PointerInputEventHandler {})
+
+ rule.setContent {
+ val capturedKey = remember { mutableStateOf("capturedKey") }
+
+ val lambdasResult =
+ createPointerInputHandlersThatCaptureWithCompose(capturedKey) {
+ cancelled.value = true
+ }
+ val lambdasCopyResult =
+ createPointerInputHandlersThatCaptureWithCompose(capturedKey) {
+ cancelled.value = true
+ }
+ lambda1 = lambdasResult.first
+ lambda2 = lambdasResult.second
+ lambda1Copy = lambdasCopyResult.first
+ lambda2Copy = lambdasCopyResult.second
+
+ Box(
+ Modifier.testTag(tag)
+ .fillMaxSize()
+ .pointerInput(key1 = Unit, key2 = Unit, block = block)
+ )
+ }
+
+ rule.runOnIdle {
+ assertThat(lambda1::class).isEqualTo(lambda1Copy::class)
+ assertThat(lambda2::class).isEqualTo(lambda2Copy::class)
+ }
+
+ block = lambda1
+
+ rule.onNodeWithTag(tag).performClick()
+
+ rule.runOnIdle { assertThat(cancelled.value).isFalse() }
+
+ // The keys have not changed, BUT the class are different between lambda1 and lambda2, so
+ // it will trigger a cancellation of lambda1.
+ block = lambda2
+
+ rule.runOnIdle { assertThat(cancelled.value).isTrue() }
+
+ // Reset
+ block = lambda1
+ cancelled.value = false
+
+ rule.onNodeWithTag(tag).performClick()
+
+ rule.runOnIdle { assertThat(cancelled.value).isFalse() }
+
+ // The keys have not changed AND the class between lambda1 and lambda1Copy are the same, so
+ // it will NOT trigger a cancellation.
+ block = lambda1Copy
+ rule.onNodeWithTag(tag).performClick()
+ rule.runOnIdle { assertThat(cancelled.value).isFalse() }
}
// Tests pointerInput with bad pointer data
@@ -1308,3 +1531,70 @@
)
}
}
+
+// The return type (PointerInputEventHandler) is actually an interface, so Kotlin will create class
+// type based on calling location/order.
+// NOTE: Because the PointerInputEventHandler is passed as a parameter, the created class will be
+// created at the call site where this function is called, NOT within this function where the
+// parameter is defined! That means you will get a different class for every unique place this
+// function is called (the same way [.pointerInput's] `block` parameter operates).
+private fun createPointerInputEventHandlerReturnTypeInterface(
+ lambda: PointerInputEventHandler
+): PointerInputEventHandler {
+ return lambda
+}
+
+// The return type ([PointerInputEventHandler]) is actually an interface, and because the function
+// creates it inside the method, the call site will always be the same no matter where this
+// function is called externally. Thus, the same class will be used for all instances vs. a new
+// class for each call which is the normal case when using `.pointerInput()`.
+// More details can be found in test functions.
+private fun createPointerInputEventHandlerWithSameClassEverytime(
+ lambda: suspend PointerInputScope.() -> Unit
+): PointerInputEventHandler {
+ return PointerInputEventHandler { with(this) { lambda() } }
+}
+
+// Same as above but creates multiple [PointerInputEventHandler]s and captures.
+private fun createPointerInputHandlersThatCapture(
+ lambda1CancellationLambda: () -> Unit
+): Pair<PointerInputEventHandler, PointerInputEventHandler> {
+ val lambda1 = PointerInputEventHandler {
+ try {
+ suspendCancellableCoroutine<Unit> {}
+ } catch (e: CancellationException) {
+ lambda1CancellationLambda()
+ }
+ }
+
+ val lambda2 = PointerInputEventHandler {}
+
+ return Pair(lambda1, lambda2)
+}
+
+// Same as above but creates multiple [PointerInputEventHandler]s and captures with Compose.
+@Composable
+private fun createPointerInputHandlersThatCaptureWithCompose(
+ key: MutableState<String>,
+ lambda1CancellationLambda: () -> Unit
+): Pair<PointerInputEventHandler, PointerInputEventHandler> {
+
+ var capturedKey by remember { key }
+
+ val lambda1 = PointerInputEventHandler {
+ println("$this")
+ println("\tcapturedKey: $capturedKey")
+ try {
+ suspendCancellableCoroutine<Unit> {}
+ } catch (e: CancellationException) {
+ lambda1CancellationLambda()
+ }
+ }
+
+ val lambda2 = PointerInputEventHandler {
+ println("$this")
+ println("\tcapturedKey: $capturedKey")
+ }
+
+ return Pair(lambda1, lambda2)
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ComposeViewLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ComposeViewLayoutTest.kt
index 728f23b..4cb23b0 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ComposeViewLayoutTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ComposeViewLayoutTest.kt
@@ -24,6 +24,7 @@
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -31,11 +32,17 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import java.util.Locale
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -114,4 +121,37 @@
assertThat(height).isEqualTo(2000)
}
}
+
+ @Test
+ fun rootViewConfiguration() {
+ val tag = "myLayout"
+ rule.setContent { Box(Modifier.size(10.dp).testTag(tag)) }
+ assertThat(rule.onRoot().fetchSemanticsNode().layoutInfo.viewConfiguration)
+ .isSameInstanceAs(
+ rule.onNodeWithTag(tag).fetchSemanticsNode().layoutInfo.viewConfiguration
+ )
+ }
+
+ @Test
+ fun rootLayoutDirectionLtr() {
+ rule.runOnUiThread {
+ val resources = rule.activity.resources
+ val configuration = resources.configuration
+ configuration.setLayoutDirection(Locale.US)
+ }
+ rule.setContent { Box(Modifier.size(10.dp)) }
+ assertThat(rule.onRoot().fetchSemanticsNode().layoutInfo.layoutDirection)
+ .isEqualTo(LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun rootLayoutDirectionRtl() {
+ rule.runOnUiThread {
+ val configuration = rule.activity.resources.configuration
+ configuration.setLayoutDirection(Locale("fa"))
+ }
+ rule.setContent { Box(Modifier.size(10.dp)) }
+ assertThat(rule.onRoot().fetchSemanticsNode().layoutInfo.layoutDirection)
+ .isEqualTo(LayoutDirection.Rtl)
+ }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
index c4ad2bf..cde7762 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
@@ -452,7 +452,7 @@
// closeConnection is only supported on API 24+
@SdkSuppress(minSdkVersion = 24)
@Test
- fun connectionClosed_whenCreateConnectionCalledAgain() {
+ fun connectionNotClosed_whenCreateConnectionCalledAgain() {
class TestConnection(view: View) : BaseInputConnection(view, true) {
var closeCalls = 0
@@ -484,15 +484,15 @@
assertThat(connections).hasSize(2)
val connection2 = connections.last()
- assertThat(connection1.closeCalls).isEqualTo(1)
+ assertThat(connection1.closeCalls).isEqualTo(0)
assertThat(connection2.closeCalls).isEqualTo(0)
hostView.onCreateInputConnection(EditorInfo())
assertThat(connections).hasSize(3)
val connection3 = connections.last()
- assertThat(connection1.closeCalls).isEqualTo(1)
- assertThat(connection2.closeCalls).isEqualTo(1)
+ assertThat(connection1.closeCalls).isEqualTo(0)
+ assertThat(connection2.closeCalls).isEqualTo(0)
assertThat(connection3.closeCalls).isEqualTo(0)
}
@@ -501,7 +501,7 @@
@SdkSuppress(minSdkVersion = 24)
@Test
- fun innerSessionCanceled_whenIsolatedFromOuterSession_whenConnectionClosed() {
+ fun innerSessionNotCanceled_whenIsolatedFromOuterSession_whenConnectionClosed() {
setupContent()
lateinit var innerJob: Job
val outerJob =
@@ -520,15 +520,14 @@
}
rule.runOnIdle {
- // Outer job isn't canceled, only the inner one.
assertThat(outerJob.isActive).isTrue()
- assertThat(innerJob.isActive).isFalse()
+ assertThat(innerJob.isActive).isTrue()
}
}
@SdkSuppress(minSdkVersion = 24)
@Test
- fun cancellationPropagates_whenConnectionClosed() {
+ fun cancellationDoesNotPropagate_whenConnectionClosed() {
setupContent()
val sessionJob =
coroutineScope.launch {
@@ -541,7 +540,7 @@
connection.closeConnection()
}
- rule.runOnIdle { assertThat(sessionJob.isActive).isFalse() }
+ rule.runOnIdle { assertThat(sessionJob.isActive).isTrue() }
}
@Test
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AutofillUtils.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AutofillUtils.android.kt
index fbfd6d5e..dc2dc2f 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AutofillUtils.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AutofillUtils.android.kt
@@ -238,5 +238,6 @@
Role.RadioButton -> "android.widget.RadioButton"
Role.Image -> "android.widget.ImageView"
Role.DropdownList -> "android.widget.Spinner"
+ Role.NumberPicker -> "android.widget.NumberPicker"
else -> ClassName
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index b4ecd2b..88a5f35 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -410,10 +410,28 @@
private val canvasHolder = CanvasHolder()
+ // Backed by mutableStateOf so that the ambient provider recomposes when it changes
+ override var layoutDirection by
+ mutableStateOf(
+ // We don't use the attached View's layout direction here since that layout direction
+ // may not
+ // be resolved since composables may be composed without attaching to the RootViewImpl.
+ // In Jetpack Compose, use the locale layout direction (i.e. layoutDirection came from
+ // configuration) as a default layout direction.
+ toLayoutDirection(context.resources.configuration.layoutDirection)
+ ?: LayoutDirection.Ltr
+ )
+ private set
+
+ override val viewConfiguration: ViewConfiguration =
+ AndroidViewConfiguration(android.view.ViewConfiguration.get(context))
+
override val root =
LayoutNode().also {
it.measurePolicy = RootMeasurePolicy
it.density = density
+ it.layoutDirection = layoutDirection
+ it.viewConfiguration = viewConfiguration
// Composed modifiers cannot be added here directly
it.modifier =
Modifier.then(semanticsModifier)
@@ -528,9 +546,6 @@
override val measureIteration: Long
get() = measureAndLayoutDelegate.measureIteration
- override val viewConfiguration: ViewConfiguration =
- AndroidViewConfiguration(android.view.ViewConfiguration.get(context))
-
override val hasPendingMeasureOrLayout
get() = measureAndLayoutDelegate.hasPendingMeasureOrLayout
@@ -637,19 +652,6 @@
private val Configuration.fontWeightAdjustmentCompat: Int
get() = if (SDK_INT >= S) fontWeightAdjustment else 0
- // Backed by mutableStateOf so that the ambient provider recomposes when it changes
- override var layoutDirection by
- mutableStateOf(
- // We don't use the attached View's layout direction here since that layout direction
- // may not
- // be resolved since composables may be composed without attaching to the RootViewImpl.
- // In Jetpack Compose, use the locale layout direction (i.e. layoutDirection came from
- // configuration) as a default layout direction.
- toLayoutDirection(context.resources.configuration.layoutDirection)
- ?: LayoutDirection.Ltr
- )
- private set
-
/** Provide haptic feedback to the user. Use the Android version of haptic feedback. */
override val hapticFeedBack: HapticFeedback = PlatformHapticFeedback(this)
@@ -818,12 +820,12 @@
}
/**
- * Avoid Android 8 crash by not traversing assist structure. Autofill assistStructure will be
- * dispatched via `dispatchProvideAutofillStructure`, not this method. See b/251152083 for more
- * details.
+ * Avoid crash by not traversing assist structure. Autofill assistStructure will be dispatched
+ * via `dispatchProvideAutofillStructure` from Android 8 and on. See b/251152083 and b/320768586
+ * more details.
*/
override fun dispatchProvideStructure(structure: ViewStructure) {
- if (SDK_INT == 26 || SDK_INT == 27) {
+ if (SDK_INT in 23..27) {
AndroidComposeViewAssistHelperMethodsO.setClassName(structure, view)
} else {
super.dispatchProvideStructure(structure)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidPlatformTextInputSession.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidPlatformTextInputSession.android.kt
index 6fc34fa..df73f76 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidPlatformTextInputSession.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidPlatformTextInputSession.android.kt
@@ -21,9 +21,11 @@
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
+import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.SessionMutex
import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.WeakReference
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.input.NullableInputConnectionWrapper
import androidx.compose.ui.text.input.TextInputService
@@ -67,8 +69,11 @@
override suspend fun startInputMethod(request: PlatformTextInputMethodRequest): Nothing =
methodSessionMutex.withSessionCancellingPrevious(
- sessionInitializer = { coroutineScope ->
- InputMethodSession(request, onConnectionClosed = { coroutineScope.cancel() })
+ sessionInitializer = {
+ InputMethodSession(
+ request = request,
+ onAllConnectionsClosed = { coroutineScope.cancel() }
+ )
}
) { methodSession ->
@Suppress("RemoveExplicitTypeArguments")
@@ -105,13 +110,16 @@
* [AndroidPlatformTextInputSession]'s input method session. Instances of this class correspond to
* calls to [AndroidPlatformTextInputSession.startInputMethod]. This class ensures that old
* connections are disposed before new ones are created.
+ *
+ * @param onAllConnectionsClosed Called when all created [InputConnection]s receive
+ * [InputConnection.closeConnection] call.
*/
private class InputMethodSession(
private val request: PlatformTextInputMethodRequest,
- private val onConnectionClosed: () -> Unit
+ private val onAllConnectionsClosed: () -> Unit
) {
private val lock = Any()
- private var connection: NullableInputConnectionWrapper? = null
+ private var connections = mutableVectorOf<WeakReference<NullableInputConnectionWrapper>>()
private var disposed = false
val isActive: Boolean
@@ -127,28 +135,48 @@
fun createInputConnection(outAttrs: EditorInfo): InputConnection? {
synchronized(lock) {
if (disposed) return null
- // Manually close the delegate in case the system won't until later.
- connection?.disposeDelegate()
+
+ // Do not manually dispose a previous InputConnection until it's collected by the GC or
+ // an explicit call is received to its `onConnectionClosed` callback.
val connectionDelegate = request.createInputConnection(outAttrs)
return NullableInputConnectionWrapper(
delegate = connectionDelegate,
- onConnectionClosed = onConnectionClosed
+ onConnectionClosed = { closedConnection ->
+ // We should not cancel any ongoing input session because connection is
+ // closed
+ // from InputConnection. This may happen at any time and does not indicate
+ // whether the system is trying to stop an input session. The platform may
+ // request a new InputConnection immediately after closing this one.
+ // Instead we should just clear all the resources used by this
+ // inputConnection,
+ // because the platform guarantees that it will never reuse a closed
+ // connection.
+ closedConnection.disposeDelegate()
+ val removeIndex = connections.indexOfFirst { it == closedConnection }
+ if (removeIndex >= 0) connections.removeAt(removeIndex)
+ if (connections.isEmpty()) {
+ onAllConnectionsClosed()
+ }
+ }
)
- .also { connection = it }
+ .also { connections.add(WeakReference(it)) }
}
}
/**
* Disposes the current [InputConnection]. After calling this method, all future calls to
* [createInputConnection] will return null.
+ *
+ * This function is only called from coroutine cancellation routine so it's not required to
+ * cancel the coroutine from here.
*/
fun dispose() {
synchronized(lock) {
// Manually close the delegate in case the system forgets to.
disposed = true
- connection?.disposeDelegate()
- connection = null
+ connections.forEach { it.get()?.disposeDelegate() }
+ connections.clear()
}
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/PlatformTextInputMethodRequest.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/PlatformTextInputMethodRequest.android.kt
index 0297a8d..5bafc46 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/PlatformTextInputMethodRequest.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/PlatformTextInputMethodRequest.android.kt
@@ -29,25 +29,30 @@
/**
* Called when the platform requests an [InputConnection] via [View.onCreateInputConnection].
*
- * This method makes stricter ordering guarantees about the lifetime of the returned
+ * This method makes relatively stricter ordering guarantees about the lifetime of the returned
* [InputConnection] than Android does, to make working with connections simpler. Namely, it
* guarantees:
- * - For a given [PlatformTextInputMethodRequest] instance, only one [InputConnection] will ever
- * be active at a time.
- * - References to an [InputConnection] will be cleared as soon as the connection becomes
- * inactive. Even if Android leaks its reference to the connection, the connection returned
- * from this method will not be leaked.
- * - On API levels that support [InputConnection.closeConnection] (24+), a connection will
- * always be closed before a new connection is requested.
+ * - References to an [InputConnection] will be cleared as soon as the connection get closed via
+ * [InputConnection.closeConnection]. Even if Android leaks its reference to the connection,
+ * the connection returned from this method will not be leaked.
+ *
+ * However it does not guarantee that only one [InputConnection] will ever be active at a time
+ * for a given [PlatformTextInputMethodRequest] instance. On the other hand Android platform
+ * does guarantee that even though the platform may create multiple [InputConnection]s, only one
+ * of them will ever communicate with the app, invalidating any other [InputConnection] that
+ * remained open at the time of communication.
*
* Android may call [View.onCreateInputConnection] multiple times for the same session – each
- * system call will result in a 1:1 call to this method, although the old connection will always
- * be closed first.
+ * system call will result in a 1:1 call to this method. Unfortunately Android platform may
+ * decide to use an earlier [InputConnection] returned from this function, invalidating the ones
+ * that were created later. Please do not rely on the order of calls to this function.
*
* @param outAttributes The [EditorInfo] from [View.onCreateInputConnection].
* @return The [InputConnection] that will be used to talk to the IME as long as the session is
* active. This connection will not receive any calls after the requesting coroutine is
* cancelled.
*/
+ // Please take a look at go/text-input-session-android-gotchas to learn more about corner cases
+ // of Android InputConnection management
fun createInputConnection(outAttributes: EditorInfo): InputConnection
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt
index 58d2c72..22c4b62 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt
@@ -122,6 +122,7 @@
Role.RadioButton -> "android.widget.RadioButton"
Role.Image -> "android.widget.ImageView"
Role.DropdownList -> "android.widget.Spinner"
+ Role.NumberPicker -> "android.widget.NumberPicker"
else -> null
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/NullableInputConnectionWrapper.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/NullableInputConnectionWrapper.android.kt
index c2956a7..dc8c5ed 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/NullableInputConnectionWrapper.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/NullableInputConnectionWrapper.android.kt
@@ -40,11 +40,12 @@
* `disposeDelegate` or `closeConnection` are called.
* @param onConnectionClosed A callback that will be invoked the first time `closeConnection` is
* called. Will not be invoked by `disposeDelegate`, and will not be invoked if `disposeDelegate`
- * is called before `closeConnection`.
+ * is called before `closeConnection`. Passed in [NullableInputConnectionWrapper] is the same
+ * instance that is created here.
*/
internal fun NullableInputConnectionWrapper(
delegate: InputConnection,
- onConnectionClosed: () -> Unit
+ onConnectionClosed: (NullableInputConnectionWrapper) -> Unit
): NullableInputConnectionWrapper =
when {
Build.VERSION.SDK_INT >= 34 ->
@@ -82,7 +83,7 @@
private open class NullableInputConnectionWrapperApi21(
delegate: InputConnection,
- private val onConnectionClosed: () -> Unit
+ private val onConnectionClosed: (NullableInputConnectionWrapper) -> Unit
) : NullableInputConnectionWrapper {
protected var delegate: InputConnection? = delegate
@@ -98,7 +99,7 @@
final override fun closeConnection() {
delegate?.let {
disposeDelegate()
- onConnectionClosed()
+ onConnectionClosed(this)
}
}
@@ -174,7 +175,7 @@
@RequiresApi(24)
private open class NullableInputConnectionWrapperApi24(
delegate: InputConnection,
- onConnectionClosed: () -> Unit
+ onConnectionClosed: (NullableInputConnectionWrapper) -> Unit
) : NullableInputConnectionWrapperApi21(delegate, onConnectionClosed) {
final override fun deleteSurroundingTextInCodePoints(p0: Int, p1: Int): Boolean =
@@ -190,7 +191,7 @@
@RequiresApi(25)
private open class NullableInputConnectionWrapperApi25(
delegate: InputConnection,
- onConnectionClosed: () -> Unit
+ onConnectionClosed: (NullableInputConnectionWrapper) -> Unit
) : NullableInputConnectionWrapperApi24(delegate, onConnectionClosed) {
final override fun commitContent(p0: InputContentInfo, p1: Int, p2: Bundle?): Boolean =
@@ -200,7 +201,7 @@
@RequiresApi(34)
private open class NullableInputConnectionWrapperApi34(
delegate: InputConnection,
- onConnectionClosed: () -> Unit
+ onConnectionClosed: (NullableInputConnectionWrapper) -> Unit
) : NullableInputConnectionWrapperApi25(delegate, onConnectionClosed) {
final override fun performHandwritingGesture(
gesture: HandwritingGesture,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index d909400..81e55d6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -183,38 +183,17 @@
fun Modifier.pointerInput(block: suspend PointerInputScope.() -> Unit): Modifier =
error(PointerInputModifierNoParamError)
-/**
- * Create a modifier for processing pointer input within the region of the modified element.
- *
- * [pointerInput] [block]s may call [PointerInputScope.awaitPointerEventScope] to install a pointer
- * input handler that can [AwaitPointerEventScope.awaitPointerEvent] to receive and consume pointer
- * input events. Extension functions on [PointerInputScope] or [AwaitPointerEventScope] may be
- * defined to perform higher-level gesture detection. The pointer input handling [block] will be
- * cancelled and **re-started** when [pointerInput] is recomposed with a different [key1].
- *
- * When a [pointerInput] modifier is created by composition, if [block] captures any local variables
- * to operate on, two patterns are common for working with changes to those variables depending on
- * the desired behavior.
- *
- * Specifying the captured value as a [key][key1] parameter will cause [block] to cancel and restart
- * from the beginning if the value changes:
- *
- * @sample androidx.compose.ui.samples.keyedPointerInputModifier
- *
- * If [block] should **not** restart when a captured value is changed but the value should still be
- * updated for its next use, use
- * [rememberUpdatedState][androidx.compose.runtime.rememberUpdatedState] to update a value holder
- * that is accessed by [block]:
- *
- * @sample androidx.compose.ui.samples.rememberedUpdatedParameterPointerInputModifier
- *
- * ***Note*** Any removal operations on Android Views from `pointerInput` should wrap the `block` in
- * a `post { }` block to guarantee the event dispatch completes before executing the removal. (You
- * do not need to do this when removing a composable because Compose guarantees it completes via the
- * snapshot state system.)
- */
+@Deprecated(
+ "This function is deprecated. Use the PointerInputEventHandler block variation instead",
+ level = DeprecationLevel.HIDDEN,
+ replaceWith =
+ ReplaceWith(
+ "pointerInput(key1 = key1, pointerInputEventHandler = block)",
+ "androidx.compose.ui.input.pointer.Modifier.pointerInput"
+ )
+)
fun Modifier.pointerInput(key1: Any?, block: suspend PointerInputScope.() -> Unit): Modifier =
- this then SuspendPointerInputElement(key1 = key1, pointerInputHandler = block)
+ this then SuspendPointerInputElement(key1 = key1, pointerInputEventHandler = block)
/**
* Create a modifier for processing pointer input within the region of the modified element.
@@ -223,7 +202,8 @@
* input handler that can [AwaitPointerEventScope.awaitPointerEvent] to receive and consume pointer
* input events. Extension functions on [PointerInputScope] or [AwaitPointerEventScope] may be
* defined to perform higher-level gesture detection. The pointer input handling [block] will be
- * cancelled and **re-started** when [pointerInput] is recomposed with a different [key1] or [key2].
+ * cancelled and **re-started** when [pointerInput] is recomposed with a different [key1] or the
+ * [block] class is different.
*
* When a [pointerInput] modifier is created by composition, if [block] captures any local variables
* to operate on, two patterns are common for working with changes to those variables depending on
@@ -246,12 +226,24 @@
* do not need to do this when removing a composable because Compose guarantees it completes via the
* snapshot state system.)
*/
+fun Modifier.pointerInput(key1: Any?, block: PointerInputEventHandler): Modifier =
+ this then SuspendPointerInputElement(key1 = key1, pointerInputEventHandler = block)
+
+@Deprecated(
+ "This function is deprecated. Use the PointerInputEventHandler block variation instead",
+ level = DeprecationLevel.HIDDEN,
+ replaceWith =
+ ReplaceWith(
+ "pointerInput(key1 = key1, key2 = key2, pointerInputEventHandler = block)",
+ "androidx.compose.ui.input.pointer.Modifier.pointerInput"
+ )
+)
fun Modifier.pointerInput(
key1: Any?,
key2: Any?,
block: suspend PointerInputScope.() -> Unit
): Modifier =
- this then SuspendPointerInputElement(key1 = key1, key2 = key2, pointerInputHandler = block)
+ this then SuspendPointerInputElement(key1 = key1, key2 = key2, pointerInputEventHandler = block)
/**
* Create a modifier for processing pointer input within the region of the modified element.
@@ -260,7 +252,56 @@
* input handler that can [AwaitPointerEventScope.awaitPointerEvent] to receive and consume pointer
* input events. Extension functions on [PointerInputScope] or [AwaitPointerEventScope] may be
* defined to perform higher-level gesture detection. The pointer input handling [block] will be
- * cancelled and **re-started** when [pointerInput] is recomposed with any different [keys].
+ * cancelled and **re-started** when [pointerInput] is recomposed with a different [key1] or [key2],
+ * or the [block] class is different.
+ *
+ * When a [pointerInput] modifier is created by composition, if [block] captures any local variables
+ * to operate on, two patterns are common for working with changes to those variables depending on
+ * the desired behavior.
+ *
+ * Specifying the captured value as a [key][key1] parameter will cause [block] to cancel and restart
+ * from the beginning if the value changes:
+ *
+ * @sample androidx.compose.ui.samples.keyedPointerInputModifier
+ *
+ * If [block] should **not** restart when a captured value is changed but the value should still be
+ * updated for its next use, use
+ * [rememberUpdatedState][androidx.compose.runtime.rememberUpdatedState] to update a value holder
+ * that is accessed by [block]:
+ *
+ * @sample androidx.compose.ui.samples.rememberedUpdatedParameterPointerInputModifier
+ *
+ * ***Note*** Any removal operations on Android Views from `pointerInput` should wrap the `block` in
+ * a `post { }` block to guarantee the event dispatch completes before executing the removal. (You
+ * do not need to do this when removing a composable because Compose guarantees it completes via the
+ * snapshot state system.)
+ */
+fun Modifier.pointerInput(key1: Any?, key2: Any?, block: PointerInputEventHandler): Modifier =
+ this then SuspendPointerInputElement(key1 = key1, key2 = key2, pointerInputEventHandler = block)
+
+@Deprecated(
+ "This function is deprecated. Use the PointerInputEventHandler block variation instead",
+ level = DeprecationLevel.HIDDEN,
+ replaceWith =
+ ReplaceWith(
+ "pointerInput(keys = keys, pointerInputEventHandler = block)",
+ "androidx.compose.ui.input.pointer.Modifier.pointerInput"
+ )
+)
+fun Modifier.pointerInput(
+ vararg keys: Any?,
+ block: suspend PointerInputScope.() -> Unit
+): Modifier = this then SuspendPointerInputElement(keys = keys, pointerInputEventHandler = block)
+
+/**
+ * Create a modifier for processing pointer input within the region of the modified element.
+ *
+ * [pointerInput] [block]s may call [PointerInputScope.awaitPointerEventScope] to install a pointer
+ * input handler that can [AwaitPointerEventScope.awaitPointerEvent] to receive and consume pointer
+ * input events. Extension functions on [PointerInputScope] or [AwaitPointerEventScope] may be
+ * defined to perform higher-level gesture detection. The pointer input handling [block] will be
+ * cancelled and **re-started** when [pointerInput] is recomposed with any different [keys] or the
+ * [block] class is different.
*
* When a [pointerInput] modifier is created by composition, if [block] captures any local variables
* to operate on, two patterns are common for working with changes to those variables depending on
@@ -283,31 +324,37 @@
* do not need to do this when removing a composable because Compose guarantees it completes via the
* snapshot state system.)
*/
-fun Modifier.pointerInput(
- vararg keys: Any?,
- block: suspend PointerInputScope.() -> Unit
-): Modifier = this then SuspendPointerInputElement(keys = keys, pointerInputHandler = block)
+fun Modifier.pointerInput(vararg keys: Any?, block: PointerInputEventHandler): Modifier =
+ this then SuspendPointerInputElement(keys = keys, pointerInputEventHandler = block)
+
+/*
+ * Represents the 'block' lambda passed into [Modifier.pointerInput]. It's used to receive and
+ * consume pointer input events.
+ */
+fun interface PointerInputEventHandler {
+ suspend operator fun PointerInputScope.invoke()
+}
internal class SuspendPointerInputElement(
val key1: Any? = null,
val key2: Any? = null,
val keys: Array<out Any?>? = null,
- val pointerInputHandler: suspend PointerInputScope.() -> Unit
+ val pointerInputEventHandler: PointerInputEventHandler
) : ModifierNodeElement<SuspendingPointerInputModifierNodeImpl>() {
override fun InspectorInfo.inspectableProperties() {
name = "pointerInput"
properties["key1"] = key1
properties["key2"] = key2
properties["keys"] = keys
- properties["pointerInputHandler"] = pointerInputHandler
+ properties["pointerInputEventHandler"] = pointerInputEventHandler
}
override fun create(): SuspendingPointerInputModifierNodeImpl {
- return SuspendingPointerInputModifierNodeImpl(key1, key2, keys, pointerInputHandler)
+ return SuspendingPointerInputModifierNodeImpl(key1, key2, keys, pointerInputEventHandler)
}
override fun update(node: SuspendingPointerInputModifierNodeImpl) {
- node.update(key1, key2, keys, pointerInputHandler)
+ node.update(key1, key2, keys, pointerInputEventHandler)
}
override fun equals(other: Any?): Boolean {
@@ -321,20 +368,39 @@
if (!keys.contentEquals(other.keys)) return false
} else if (other.keys != null) return false
- return pointerInputHandler === other.pointerInputHandler
+ return pointerInputEventHandler === other.pointerInputEventHandler
}
override fun hashCode(): Int {
var result = key1?.hashCode() ?: 0
result = 31 * result + (key2?.hashCode() ?: 0)
result = 31 * result + (keys?.contentHashCode() ?: 0)
- result = 31 * result + pointerInputHandler.hashCode()
+ result = 31 * result + pointerInputEventHandler.hashCode()
return result
}
}
private val EmptyPointerEvent = PointerEvent(emptyList())
+/** Deprecated, use [SuspendingPointerInputModifierNode] instead. */
+@Deprecated(
+ message =
+ "This function is deprecated. Use 'SuspendingPointerInputModifierNode' with the" +
+ "PointerInputEventHandler instead.",
+ level = DeprecationLevel.HIDDEN,
+ replaceWith =
+ ReplaceWith(
+ "SuspendingPointerInputModifierNode { pointerInputEventHandler }",
+ "androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode"
+ )
+)
+@Suppress("DEPRECATION")
+fun SuspendingPointerInputModifierNode(
+ pointerInputHandler: suspend PointerInputScope.() -> Unit
+): SuspendingPointerInputModifierNode {
+ return SuspendingPointerInputModifierNodeImpl(null, null, null, pointerInputHandler)
+}
+
/**
* Supports suspending pointer event handling. This is used by [pointerInput], so in most cases you
* should just use [pointerInput] for suspending pointer input. Creating a
@@ -342,9 +408,9 @@
* suspending pointer input as part of the implementation of a complex [Modifier.Node].
*/
fun SuspendingPointerInputModifierNode(
- pointerInputHandler: suspend PointerInputScope.() -> Unit
+ pointerInputEventHandler: PointerInputEventHandler
): SuspendingPointerInputModifierNode {
- return SuspendingPointerInputModifierNodeImpl(null, null, null, pointerInputHandler)
+ return SuspendingPointerInputModifierNodeImpl(null, null, null, pointerInputEventHandler)
}
/**
@@ -358,14 +424,35 @@
* Handler for pointer input events. When changed, any previously executing pointerInputHandler
* will be canceled.
*/
+ @Deprecated(
+ message = "This property is deprecated. Use 'pointerInputEventHandler' instead.",
+ level = DeprecationLevel.ERROR,
+ replaceWith =
+ ReplaceWith(
+ "pointerInputEventHandler",
+ "androidx.compose.ui.input.pointer." +
+ "SuspendingPointerInputModifierNode.pointerInputEventHandler"
+ )
+ )
var pointerInputHandler: suspend PointerInputScope.() -> Unit
/**
+ * Handler for pointer input events. When changed, any previously executing
+ * pointerInputEventHandler will be canceled.
+ */
+ // Supports more dynamic use cases than previous functional type version.
+ // NOTE: If you implement this interface, replace the default implementation. For more
+ // technical details, see aosp/3070509
+ var pointerInputEventHandler: PointerInputEventHandler
+ get() = TODO("pointerInputEventHandler must be implemented (get()).")
+ set(value) = TODO("pointerInputEventHandler must be implemented (set($value)).")
+
+ /**
* Resets the underlying coroutine used to run the handler for input pointer events. This should
* be called whenever a large change has been made that forces the gesture detection to be
* completely invalid.
*
- * For example, if [pointerInputHandler] has different modes for detecting a gesture (long
+ * For example, if [pointerInputEventHandler] has different modes for detecting a gesture (long
* press, double click, etc.), and by switching the modes, any currently-running gestures are no
* longer valid.
*/
@@ -375,7 +462,7 @@
/**
* Implementation notes: This class does a lot of lifting. [PointerInputModifierNode] receives,
* interprets, and, consumes [PointerInputChange]s while the state (and the coroutineScope used to
- * execute [pointerInputHandler]) is retained in [Modifier.Node].
+ * execute [pointerInputEventHandler]) is retained in [Modifier.Node].
*
* [SuspendingPointerInputModifierNodeImpl] implements the [PointerInputScope] used to offer the
* [Modifier.pointerInput] DSL and provides the [Density] from [LocalDensity] lazily from the layout
@@ -388,49 +475,47 @@
private var key1: Any? = null,
private var key2: Any? = null,
private var keys: Array<out Any?>? = null,
- pointerInputHandler: suspend PointerInputScope.() -> Unit
+ pointerInputEventHandler: PointerInputEventHandler
) : Modifier.Node(), SuspendingPointerInputModifierNode, PointerInputScope, Density {
-
- internal fun update(
+ @Deprecated("Exists to maintain compatibility with previous API shape")
+ constructor(
key1: Any?,
key2: Any?,
keys: Array<out Any?>?,
- pointerInputHandler: suspend PointerInputScope.() -> Unit
+ pointerInputEvent: suspend PointerInputScope.() -> Unit
+ ) : this(
+ key1 = key1,
+ key2 = key2,
+ keys = keys,
+ pointerInputEventHandler = PointerInputEventHandler {} // Empty Lambda, not used.
) {
- var needsReset = false
- if (this.key1 != key1) {
- needsReset = true
- }
- this.key1 = key1
- if (this.key2 != key2) {
- needsReset = true
- }
- this.key2 = key2
- if (this.keys != null && keys == null) {
- needsReset = true
- }
- if (this.keys == null && keys != null) {
- needsReset = true
- }
- if (this.keys != null && keys != null && !keys.contentEquals(this.keys)) {
- needsReset = true
- }
- this.keys = keys
- if (needsReset) {
- resetPointerInputHandler()
- }
- // Avoids calling resetPointerInputHandler when setting this if no keys have changed
- _pointerInputHandler = pointerInputHandler
+ // If the _deprecatedPointerInputHandler is set, we will use that instead of the
+ // pointerInputEventHandler (why empty lambda above doesn't matter).
+ _deprecatedPointerInputHandler = pointerInputEvent
}
- private var _pointerInputHandler = pointerInputHandler
+ // Previously used to execute pointer input handlers (now pointerInputEventHandler covers that).
+ // This exists purely for backwards compatibility.
+ private var _deprecatedPointerInputHandler: (suspend PointerInputScope.() -> Unit)? = null
- override var pointerInputHandler
+ // Main handler for pointer input events
+ private var _pointerInputEventHandler = pointerInputEventHandler
+
+ @Deprecated("Super property deprecated")
+ override var pointerInputHandler: suspend PointerInputScope.() -> Unit
+ get() = _deprecatedPointerInputHandler ?: {}
set(value) {
resetPointerInputHandler()
- _pointerInputHandler = value
+ _deprecatedPointerInputHandler = value
}
- get() = _pointerInputHandler
+
+ override var pointerInputEventHandler
+ set(value) {
+ resetPointerInputHandler()
+ _deprecatedPointerInputHandler = null
+ _pointerInputEventHandler = value
+ }
+ get() = _pointerInputEventHandler
override val density: Float
get() = requireLayoutNode().density.density
@@ -507,12 +592,12 @@
}
/**
- * This cancels the existing coroutine and essentially resets pointerInputHandler's execution.
- * Note, the pointerInputHandler still executes lazily, meaning nothing will be done again until
- * a new event comes in. More details: This is triggered from a LayoutNode if the Density or
- * ViewConfiguration change (in an older implementation using composed, these values were used
- * as keys so it would reset everything when either change, we do that manually now through this
- * function). It is also used for testing.
+ * This cancels the existing coroutine and essentially resets pointerInputEventHandler's
+ * execution. Note, the pointerInputEventHandler still executes lazily, meaning nothing will be
+ * done again until a new event comes in. More details: This is triggered from a LayoutNode if
+ * the Density or ViewConfiguration change (in an older implementation using composed, these
+ * values were used as keys so it would reset everything when either change, we do that manually
+ * now through this function). It is also used for testing.
*/
override fun resetPointerInputHandler() {
val localJob = pointerInputJob
@@ -522,6 +607,55 @@
}
}
+ internal fun update(
+ key1: Any?,
+ key2: Any?,
+ keys: Array<out Any?>?,
+ pointerInputEventHandler: PointerInputEventHandler
+ ) {
+ var needsReset = false
+
+ // key1
+ if (this.key1 != key1) {
+ needsReset = true
+ }
+ this.key1 = key1
+
+ // key2
+ if (this.key2 != key2) {
+ needsReset = true
+ }
+ this.key2 = key2
+
+ // keys
+ if (this.keys != null && keys == null) {
+ needsReset = true
+ }
+ if (this.keys == null && keys != null) {
+ needsReset = true
+ }
+ if (this.keys != null && keys != null && !keys.contentEquals(this.keys)) {
+ needsReset = true
+ }
+ this.keys = keys
+
+ // Lambda literals will have a new instance every time they are executed (even if it is from
+ // the same code location), so we can not use them as a comparison mechanism to avoid
+ // restarting pointer input handlers when they are functionally the same. However, we can
+ // get around this by using a SAM interface and a class comparison instead. (Even though the
+ // instances are different, they will have the same class type.)
+ if (this.pointerInputEventHandler::class != pointerInputEventHandler::class) {
+ needsReset = true
+ }
+
+ // Only reset when keys have changed or pointerInputEventHandler is called from a different
+ // call site (determined by class comparison).
+ if (needsReset) {
+ resetPointerInputHandler()
+ }
+ _pointerInputEventHandler = pointerInputEventHandler
+ }
+
/**
* Snapshot the current [pointerHandlers] and run [block] on each one. May not be called
* reentrant or concurrent with itself.
@@ -570,7 +704,13 @@
if (pointerInputJob == null) {
// 'start = CoroutineStart.UNDISPATCHED' required so handler doesn't miss first event.
pointerInputJob =
- coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { pointerInputHandler() }
+ coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ if (_deprecatedPointerInputHandler != null) {
+ _deprecatedPointerInputHandler!!()
+ } else {
+ with(pointerInputEventHandler) { invoke() }
+ }
+ }
}
dispatchPointerEvent(pointerEvent, pass)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/PlatformTextInputModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/PlatformTextInputModifierNode.kt
index 27774fc..3498baf 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/PlatformTextInputModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/PlatformTextInputModifierNode.kt
@@ -112,8 +112,11 @@
* - The session function throws an exception.
* - The requesting coroutine is cancelled.
* - Another session is started via this method, either from the same modifier or a different one.
- * - The system closes the connection (currently only supported on Android, and there only depending
- * on OS version).
+ * The session may remain open when:
+ * - The system closes the connection. This behavior currently only exists on Android depending on
+ * OS version. Android platform may intermittently close the active connection to immediately
+ * start it back again. In these cases the session will not be prematurely closed, so that it can
+ * serve the follow-up requests.
*
* This function should only be called from the modifier node's
* [coroutineScope][Modifier.Node.coroutineScope]. If it is not, the session will _not_
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
index 0284257..3d505bd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
@@ -670,6 +670,12 @@
* accessibility: [SemanticsActions.OnClick]
*/
val DropdownList = Role(6)
+
+ /**
+ * This element is a number picker that a user can perform gesture to adjust and select the
+ * next or previous value.
+ */
+ val NumberPicker = Role(7)
}
override fun toString() =
@@ -681,6 +687,7 @@
Tab -> "Tab"
Image -> "Image"
DropdownList -> "DropdownList"
+ NumberPicker -> "NumberPicker"
else -> "Unknown"
}
}
diff --git a/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
index 8d3c0fee..e7f7289 100644
--- a/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidInstrumentedTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
@@ -21,6 +21,7 @@
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
@@ -30,6 +31,8 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -60,12 +63,14 @@
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
+import androidx.compose.ui.util.fastRoundToInt
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -2367,6 +2372,88 @@
assertEquals(expectedEndPosition, box0Position)
}
+ @Test
+ fun testEmptyConstraintLayoutSize() =
+ with(rule.density) {
+ val rootSizePx = 200f
+
+ // Mutable state to trigger invalidations. By default simply passes original
+ // constraints.
+ // But we'll use this to test empty ConstraintLayout under different constraints
+ var transformConstraints: ((Constraints) -> Constraints) by
+ mutableStateOf({ constraints -> constraints })
+
+ // To capture measured ConstraintLayout size
+ var layoutSize = IntSize(-1, -1)
+
+ rule.setContent {
+ Column(Modifier.size(rootSizePx.toDp()).verticalScroll(rememberScrollState())) {
+ ConstraintLayout(
+ modifier =
+ Modifier.layout { measurable, constraints ->
+ // Measure policy to test ConstraintLayout under different
+ // constraints
+ val placeable =
+ measurable.measure(transformConstraints(constraints))
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ .onGloballyPositioned { layoutSize = it.size }
+ ) {
+ // Empty content
+ }
+ }
+ }
+ // For this case, the default behavior should be a ConstraintLayout of size 0x0
+ rule.waitForIdle()
+ assertEquals(IntSize.Zero, layoutSize)
+
+ // Test with min constraints
+ transformConstraints = { constraints ->
+ // Demonstrate that vertical scroll constraints propagate
+ assert(constraints.maxHeight == Constraints.Infinity)
+ constraints.copy(minWidth = 123, minHeight = 321)
+ }
+ rule.waitForIdle()
+
+ // Minimum size is preferred for empty layouts. Should not crash :)
+ assertEquals(IntSize(width = 123, height = 321), layoutSize)
+
+ // Transform to an equivalent of fillMaxSize(), which fills bounded constraints only
+ transformConstraints = { constraints ->
+ val minWidth: Int
+ val maxWidth: Int
+ val minHeight: Int
+ val maxHeight: Int
+
+ if (constraints.hasBoundedWidth) {
+ minWidth = constraints.maxWidth
+ maxWidth = constraints.maxWidth
+ } else {
+ minWidth = constraints.minWidth
+ maxWidth = constraints.maxWidth
+ }
+ if (constraints.hasBoundedHeight) {
+ minHeight = constraints.maxHeight
+ maxHeight = constraints.maxHeight
+ } else {
+ minHeight = constraints.minHeight
+ maxHeight = constraints.maxHeight
+ }
+ Constraints(
+ minWidth = minWidth,
+ maxWidth = maxWidth,
+ minHeight = minHeight,
+ maxHeight = maxHeight
+ )
+ }
+ rule.waitForIdle()
+
+ // Vertical is infinity, fillMaxSize behavior should pin it to minimum height (Zero)
+ assertEquals(IntSize(rootSizePx.fastRoundToInt(), 0), layoutSize)
+ }
+
/**
* Provides a list constraints combination for horizontal anchors: `start`, `end`,
* `absoluteLeft`, `absoluteRight`.
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
index 1f6a346..04acd28 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
@@ -1874,6 +1874,13 @@
measurables: List<Measurable>,
optimizationLevel: Int
): IntSize {
+ if (measurables.isEmpty()) {
+ // TODO(b/335524398): Behavior with zero children is unexpected. It's also inconsistent
+ // with ViewGroup, so this is a workaround to handle those cases the way it seems
+ // right for this implementation.
+ return IntSize(constraints.minWidth, constraints.minHeight)
+ }
+
// Define the size of the ConstraintLayout.
state.width(
if (constraints.hasFixedWidth) {
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeImpl.kt
index 7410702..d4d1c35 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeImpl.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeImpl.kt
@@ -80,8 +80,14 @@
override suspend fun addControlee(address: UwbAddress, parameters: RangingControleeParameters) {
val uwbAddress = com.google.android.gms.nearby.uwb.UwbAddress(address.address)
+ val uwbControleeParams =
+ com.google.android.gms.nearby.uwb.RangingControleeParameters(
+ uwbAddress,
+ parameters.subSessionId,
+ parameters.subSessionKey
+ )
try {
- uwbClient.addControlee(uwbAddress).await()
+ uwbClient.addControleeWithSessionParams(uwbControleeParams).await()
} catch (e: ApiException) {
if (e.statusCode == UwbStatusCodes.INVALID_API_CALL) {
throw IllegalStateException(
diff --git a/development/buildHealthAdviceToCsv.main.kts b/development/buildHealthAdviceToCsv.main.kts
new file mode 100755
index 0000000..40dc9c4
--- /dev/null
+++ b/development/buildHealthAdviceToCsv.main.kts
@@ -0,0 +1,222 @@
+#!/usr/bin/env kotlin
+
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * To run .kts files, follow these steps:
+ *
+ * 1. Download and install the Kotlin compiler (kotlinc). There are several ways to do this; see
+ * https://kotlinlang.org/docs/command-line.html
+ * 2. Run the script from the command line:
+ * <path_to>/kotlinc -script <script_file>.kts <arguments>
+ */
+
+@file:Repository("https://repo1.maven.org/maven2")
+@file:DependsOn("com.google.code.gson:gson:2.11.0")
+
+import java.io.File
+import java.util.Locale
+import kotlin.system.exitProcess
+import com.google.gson.Gson
+
+if (args.isEmpty() || !args.contains("-input")) {
+ println("Expected path to buildHealth Advice json file using -input.")
+ println("Provide output file path using -output. By default, output.csv will be generated " +
+ "in the current directory")
+ println("Usage ex: kotlinc -script buildHealthAdviceToCsv.main.kts -- -input " +
+ "path/to/build-health-report.json -output /path/to/output.csv")
+ exitProcess(1)
+}
+
+val input = args[1]
+if (!File(input).exists()) {
+ println("Could not find input files: $input")
+ exitProcess(1)
+}
+val inputJsonFile = File(input)
+println("Parsing ${inputJsonFile.path}...")
+
+val outputFile = if(args.contains("-output")) {
+ args[3]
+} else {
+ "output.csv"
+}
+
+val csvOutputFile = File(outputFile)
+if(csvOutputFile.exists()) {
+ csvOutputFile.delete()
+}
+
+val csvData = StringBuilder()
+val columnHeaders = listOf(
+ "ID", // leave blank
+ "Priority",
+ "Summary",
+ "HotlistIds",
+ "Assignee", // leave blank
+ "Status",
+ "ComponentPath",
+ "Reporter",
+ "FirstNote"
+)
+csvData.append(columnHeaders.joinToString(","))
+csvData.append("\n")
+
+val gson = Gson()
+val advice: Advice = gson.fromJson(inputJsonFile.readLines().first(), Advice::class.java)
+
+//list of projects we want to file issues for
+//val androidxProjects = listOf("lint", "lint-checks", "buildSrc-tests", "androidx-demos", "stableaidl", "test")
+//val flanProjects = listOf("activity", "fragment", "lifecycle", "navigation")
+
+var numProjects = 0
+advice.projectAdvice.forEach projectAdvice@{ projectAdvice ->
+ val projectPath = projectAdvice.projectPath
+ val title = "Please fix misconfigured dependencies for $projectPath"
+
+ val project = projectPath.split(":")[1]
+
+// Uncomment the following section if you want to create bugs for specific projects
+// if(project !in flanProjects) {
+// return@projectAdvice
+// }
+
+ val description = StringBuilder()
+ description.appendLine(
+ "The dependency analysis gradle plugin found some dependencies that may have been " +
+ "misconfigured. Please fix the following dependencies: \n"
+ )
+
+ val unused = mutableSetOf<String>()
+ val transitive = mutableSetOf<String>()
+ val modified = mutableSetOf<String>()
+ projectAdvice.dependencyAdvice.forEach { dependencyAdvice ->
+ val fromConfiguration = dependencyAdvice.fromConfiguration
+ val toConfiguration = dependencyAdvice.toConfiguration
+ val coordinates = dependencyAdvice.coordinates
+ val resolvedVersion = coordinates.resolvedVersion
+
+ val isCompileOnly = toConfiguration?.endsWith("compileOnly", ignoreCase = true) == true
+ val isModifyDependencyAdvice = fromConfiguration != null && toConfiguration != null
+ val isTransitiveDependencyAdvice = fromConfiguration == null && toConfiguration != null && !isCompileOnly
+ val isUnusedDependencyAdvice = fromConfiguration != null && toConfiguration == null
+
+ var identifier = if(resolvedVersion == null) {
+ "'${coordinates.identifier}'"
+ } else {
+ "'${coordinates.identifier}:${coordinates.resolvedVersion}'"
+ }
+ if (coordinates.type == "project") {
+ identifier = "project($identifier)"
+ }
+ if (isModifyDependencyAdvice) {
+ modified.add("$toConfiguration($identifier) (was $fromConfiguration)")
+ }
+ if(isTransitiveDependencyAdvice) {
+ transitive.add("$toConfiguration($identifier)")
+ }
+ if(isUnusedDependencyAdvice) {
+ unused.add("$fromConfiguration($identifier)")
+ }
+ }
+
+ if(unused.isNotEmpty()) {
+ description.appendLine("Unused dependencies which should be removed:")
+ description.appendLine("```")
+ description.appendLine(unused.sorted().joinToString(separator = "\n"))
+ description.appendLine("```")
+
+ }
+ if (transitive.isNotEmpty()) {
+ description.appendLine("These transitive dependencies can be declared directly:")
+ description.appendLine("```")
+ description.appendLine(transitive.sorted().joinToString(separator = "\n"))
+ description.appendLine("```")
+ }
+ if (modified.isNotEmpty()) {
+ description.appendLine(
+ "These dependencies can be modified to be as indicated. Please be careful " +
+ "while changing the type of dependencies since it can affect the consumers of " +
+ "this library. To learn more about the various dependency configurations, " +
+ "please visit: [dac]" +
+ "(https://developer.android.com/build/dependencies#dependency_configurations). " +
+ "Also check [Gradle's guide for dependency management]" +
+ "(https://docs.gradle.org/current/userguide/dependency_management.html).\n"
+ )
+ description.appendLine("```")
+ description.appendLine(modified.sorted().joinToString(separator = "\n"))
+ description.appendLine("```")
+ }
+
+ description.appendLine("To get more up-to-date project advice, please run:")
+ description.appendLine("```")
+ description.appendLine("./gradlew $projectPath:projectHealth")
+ description.appendLine("```")
+ val newColumns = listOf(
+ "NEW00000", // ID
+ "P2", // priority
+ title,
+ "=HYPERLINK(\"https://issuetracker.google.com/issues?q=status%3Aopen%20hotlistid%3A(5997499)\", \"Androidx misconfigured dependencies\")",
+ "", // Assignee: leave blank
+ "Assigned", // status
+ "Android > Android OS & Apps > Jetpack (androidx) > ${project.replaceFirstChar {
+ if (it.isLowerCase()) it.titlecase(
+ Locale.getDefault()
+ ) else it.toString()
+ }}",
+ "", // reporter: add your ldap here
+ description.toString()
+ )
+
+ if (projectAdvice.dependencyAdvice.isNotEmpty()) {
+ numProjects++
+ csvData.append(newColumns.joinToString(",") { data ->
+ "\"${data.replace("\"", "\"\"")}\""
+ })
+ csvData.append("\n")
+ }
+}
+
+csvOutputFile.appendText(csvData.toString())
+println("Wrote CSV output to ${csvOutputFile.path} for $numProjects projects")
+
+data class Advice(
+ val projectAdvice: List<ProjectAdvice>,
+)
+
+data class ProjectAdvice(
+ val projectPath: String,
+ val dependencyAdvice: List<DependencyAdvice>,
+ val pluginAdvice: List<PluginAdvice>,
+)
+
+data class DependencyAdvice(
+ val coordinates: Coordinates,
+ val fromConfiguration: String?,
+ val toConfiguration: String?
+)
+
+data class Coordinates(
+ val type: String,
+ val identifier: String,
+ val resolvedVersion: String?
+)
+
+data class PluginAdvice(
+ val redundantPlugin: String,
+ val reason: String
+)
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 9c2a7a5..f875878 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -301,4 +301,10 @@
# buildPrivacySandboxSdkApksForDebug (b/329101823)
Extracted sandbox SDK APK for.*
# Building XCFrameworks (b/260140834) and iOS benchmark invocation
-.*xcodebuild.*
\ No newline at end of file
+.*xcodebuild.*
+# > Task :benchmark:benchmark-darwin-samples:assembleAndroidXDarwinBenchmarksReleaseXCFramework
+objc\[[0-9]+\]: Class AMSupportURLConnectionDelegate is implemented in both /usr/lib/libauthinstall\.dylib \([0-9]+x[0-9]+f[0-9]+cf[0-9]+e[0-9]+\) and /System/Library/PrivateFrameworks/MobileDevice\.framework/Versions/A/MobileDevice \([0-9]+x[0-9]+e[0-9]+bc[0-9]+c[0-9]+\)\. One of the two will be used\. Which one is undefined\.
+objc\[[0-9]+\]: Class AMSupportURLSession is implemented in both /usr/lib/libauthinstall\.dylib \([0-9]+x[0-9]+f[0-9]+cf[0-9]+\) and /System/Library/PrivateFrameworks/MobileDevice\.framework/Versions/A/MobileDevice \([0-9]+x[0-9]+e[0-9]+bc[0-9]+\)\. One of the two will be used\. Which one is undefined\.
+# > Task :collection:collection-benchmark:assembleAndroidXDarwinBenchmarksReleaseXCFramework
+objc\[[0-9]+\]: Class AMSupportURLConnectionDelegate is implemented in both /usr/lib/libauthinstall\.dylib \([0-9]+x[0-9]+f[0-9]+cf[0-9]+e[0-9]+\) and /System/Library/PrivateFrameworks/MobileDevice\.framework/Versions/A/MobileDevice \([0-9]+x[0-9]+c[0-9]+c[0-9]+\)\. One of the two will be used\. Which one is undefined\.
+objc\[[0-9]+\]: Class AMSupportURLSession is implemented in both /usr/lib/libauthinstall\.dylib \([0-9]+x[0-9]+f[0-9]+cf[0-9]+\) and /System/Library/PrivateFrameworks/MobileDevice\.framework/Versions/A/MobileDevice \([0-9]+x[0-9]+c[0-9]+\)\. One of the two will be used\. Which one is undefined\.
\ No newline at end of file
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 8fe88af..6717548 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -89,7 +89,6 @@
kmpDocs("androidx.compose.runtime:runtime-saveable:1.7.0-beta04")
docs("androidx.compose.runtime:runtime-tracing:1.0.0-beta01")
kmpDocs("androidx.compose.ui:ui:1.7.0-beta04")
- docs("androidx.compose.ui:ui-android-stubs:1.7.0-beta04")
kmpDocs("androidx.compose.ui:ui-geometry:1.7.0-beta04")
kmpDocs("androidx.compose.ui:ui-graphics:1.7.0-beta04")
kmpDocs("androidx.compose.ui:ui-test:1.7.0-beta04")
@@ -332,23 +331,23 @@
docs("androidx.startup:startup-runtime:1.2.0-alpha02")
docs("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
// androidx.test is not hosted in androidx
- docsWithoutApiSince("androidx.test:core:1.6.0")
- docsWithoutApiSince("androidx.test:core-ktx:1.6.0")
- docsWithoutApiSince("androidx.test:monitor:1.7.0")
- docsWithoutApiSince("androidx.test:rules:1.6.0")
- docsWithoutApiSince("androidx.test:runner:1.6.0")
- docsWithoutApiSince("androidx.test.espresso:espresso-accessibility:3.6.0")
- docsWithoutApiSince("androidx.test.espresso:espresso-contrib:3.6.0")
- docsWithoutApiSince("androidx.test.espresso:espresso-core:3.6.0")
- docsWithoutApiSince("androidx.test.espresso:espresso-device:1.0.0")
- docsWithoutApiSince("androidx.test.espresso:espresso-idling-resource:3.6.0")
- docsWithoutApiSince("androidx.test.espresso:espresso-intents:3.6.0")
- docsWithoutApiSince("androidx.test.espresso:espresso-remote:3.6.0")
- docsWithoutApiSince("androidx.test.espresso:espresso-web:3.6.0")
- docsWithoutApiSince("androidx.test.espresso.idling:idling-concurrent:3.6.0")
- docsWithoutApiSince("androidx.test.espresso.idling:idling-net:3.6.0")
- docsWithoutApiSince("androidx.test.ext:junit:1.2.0")
- docsWithoutApiSince("androidx.test.ext:junit-ktx:1.2.0")
+ docsWithoutApiSince("androidx.test:core:1.6.1")
+ docsWithoutApiSince("androidx.test:core-ktx:1.6.1")
+ docsWithoutApiSince("androidx.test:monitor:1.7.1")
+ docsWithoutApiSince("androidx.test:rules:1.6.1")
+ docsWithoutApiSince("androidx.test:runner:1.6.1")
+ docsWithoutApiSince("androidx.test.espresso:espresso-accessibility:3.6.1")
+ docsWithoutApiSince("androidx.test.espresso:espresso-contrib:3.6.1")
+ docsWithoutApiSince("androidx.test.espresso:espresso-core:3.6.1")
+ docsWithoutApiSince("androidx.test.espresso:espresso-device:1.0.1")
+ docsWithoutApiSince("androidx.test.espresso:espresso-idling-resource:3.6.1")
+ docsWithoutApiSince("androidx.test.espresso:espresso-intents:3.6.1")
+ docsWithoutApiSince("androidx.test.espresso:espresso-remote:3.6.1")
+ docsWithoutApiSince("androidx.test.espresso:espresso-web:3.6.1")
+ docsWithoutApiSince("androidx.test.espresso.idling:idling-concurrent:3.6.1")
+ docsWithoutApiSince("androidx.test.espresso.idling:idling-net:3.6.1")
+ docsWithoutApiSince("androidx.test.ext:junit:1.2.1")
+ docsWithoutApiSince("androidx.test.ext:junit-ktx:1.2.1")
docsWithoutApiSince("androidx.test.ext:truth:1.6.0")
docsWithoutApiSince("androidx.test.services:storage:1.5.0")
docs("androidx.test.uiautomator:uiautomator:2.4.0-alpha01")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index acd2153..9cb8c04 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -98,7 +98,6 @@
kmpDocs(project(":compose:runtime:runtime-saveable"))
docs(project(":compose:runtime:runtime-tracing"))
kmpDocs(project(":compose:ui:ui"))
- docs(project(":compose:ui:ui-android-stubs"))
kmpDocs(project(":compose:ui:ui-geometry"))
kmpDocs(project(":compose:ui:ui-graphics"))
kmpDocs(project(":compose:ui:ui-test"))
@@ -197,7 +196,7 @@
docs(project(":hilt:hilt-navigation-compose"))
docs(project(":hilt:hilt-navigation-fragment"))
docs(project(":hilt:hilt-work"))
- docs(project(":ink:ink-brush"))
+ kmpDocs(project(":ink:ink-brush"))
docs(project(":input:input-motionprediction"))
docs(project(":interpolator:interpolator"))
docs(project(":javascriptengine:javascriptengine"))
diff --git a/docs/kmp_images/kmp_jetpad.png b/docs/kmp_images/kmp_jetpad.png
new file mode 100644
index 0000000..a96212a
--- /dev/null
+++ b/docs/kmp_images/kmp_jetpad.png
Binary files differ
diff --git a/emoji2/emoji2-bundled/api/1.5.0-beta01.txt b/emoji2/emoji2-bundled/api/1.5.0-beta01.txt
new file mode 100644
index 0000000..fab7dab
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/1.5.0-beta01.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+ public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+ ctor @Deprecated public BundledEmojiCompatConfig(android.content.Context);
+ ctor public BundledEmojiCompatConfig(android.content.Context, java.util.concurrent.Executor);
+ }
+
+}
+
diff --git a/emoji2/emoji2-bundled/api/res-1.5.0-beta01.txt b/emoji2/emoji2-bundled/api/res-1.5.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/res-1.5.0-beta01.txt
diff --git a/emoji2/emoji2-bundled/api/restricted_1.5.0-beta01.txt b/emoji2/emoji2-bundled/api/restricted_1.5.0-beta01.txt
new file mode 100644
index 0000000..fab7dab
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/restricted_1.5.0-beta01.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+ public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+ ctor @Deprecated public BundledEmojiCompatConfig(android.content.Context);
+ ctor public BundledEmojiCompatConfig(android.content.Context, java.util.concurrent.Executor);
+ }
+
+}
+
diff --git a/emoji2/emoji2-emojipicker/api/1.5.0-beta01.txt b/emoji2/emoji2-emojipicker/api/1.5.0-beta01.txt
new file mode 100644
index 0000000..8f56c18b
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/api/1.5.0-beta01.txt
@@ -0,0 +1,43 @@
+// Signature format: 4.0
+package androidx.emoji2.emojipicker {
+
+ public final class EmojiPickerView extends android.widget.FrameLayout {
+ ctor public EmojiPickerView(android.content.Context context);
+ ctor public EmojiPickerView(android.content.Context context, optional android.util.AttributeSet? attrs);
+ ctor public EmojiPickerView(android.content.Context context, optional android.util.AttributeSet? attrs, optional int defStyleAttr);
+ method public int getEmojiGridColumns();
+ method public float getEmojiGridRows();
+ method public void setEmojiGridColumns(int);
+ method public void setEmojiGridRows(float);
+ method public void setOnEmojiPickedListener(androidx.core.util.Consumer<androidx.emoji2.emojipicker.EmojiViewItem>? onEmojiPickedListener);
+ method public void setRecentEmojiProvider(androidx.emoji2.emojipicker.RecentEmojiProvider recentEmojiProvider);
+ property public final int emojiGridColumns;
+ property public final float emojiGridRows;
+ }
+
+ public final class EmojiViewItem {
+ ctor public EmojiViewItem(String emoji, java.util.List<java.lang.String> variants);
+ method public String getEmoji();
+ method public java.util.List<java.lang.String> getVariants();
+ property public final String emoji;
+ property public final java.util.List<java.lang.String> variants;
+ }
+
+ public interface RecentEmojiAsyncProvider {
+ method public com.google.common.util.concurrent.ListenableFuture<java.util.List<java.lang.String>> getRecentEmojiListAsync();
+ method public void recordSelection(String emoji);
+ }
+
+ public interface RecentEmojiProvider {
+ method public suspend Object? getRecentEmojiList(kotlin.coroutines.Continuation<? super java.util.List<java.lang.String>>);
+ method public void recordSelection(String emoji);
+ }
+
+ public final class RecentEmojiProviderAdapter implements androidx.emoji2.emojipicker.RecentEmojiProvider {
+ ctor public RecentEmojiProviderAdapter(androidx.emoji2.emojipicker.RecentEmojiAsyncProvider recentEmojiAsyncProvider);
+ method public suspend Object? getRecentEmojiList(kotlin.coroutines.Continuation<? super java.util.List<java.lang.String>>);
+ method public void recordSelection(String emoji);
+ }
+
+}
+
diff --git a/emoji2/emoji2-emojipicker/api/res-1.5.0-beta01.txt b/emoji2/emoji2-emojipicker/api/res-1.5.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/api/res-1.5.0-beta01.txt
diff --git a/emoji2/emoji2-emojipicker/api/restricted_1.5.0-beta01.txt b/emoji2/emoji2-emojipicker/api/restricted_1.5.0-beta01.txt
new file mode 100644
index 0000000..8f56c18b
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/api/restricted_1.5.0-beta01.txt
@@ -0,0 +1,43 @@
+// Signature format: 4.0
+package androidx.emoji2.emojipicker {
+
+ public final class EmojiPickerView extends android.widget.FrameLayout {
+ ctor public EmojiPickerView(android.content.Context context);
+ ctor public EmojiPickerView(android.content.Context context, optional android.util.AttributeSet? attrs);
+ ctor public EmojiPickerView(android.content.Context context, optional android.util.AttributeSet? attrs, optional int defStyleAttr);
+ method public int getEmojiGridColumns();
+ method public float getEmojiGridRows();
+ method public void setEmojiGridColumns(int);
+ method public void setEmojiGridRows(float);
+ method public void setOnEmojiPickedListener(androidx.core.util.Consumer<androidx.emoji2.emojipicker.EmojiViewItem>? onEmojiPickedListener);
+ method public void setRecentEmojiProvider(androidx.emoji2.emojipicker.RecentEmojiProvider recentEmojiProvider);
+ property public final int emojiGridColumns;
+ property public final float emojiGridRows;
+ }
+
+ public final class EmojiViewItem {
+ ctor public EmojiViewItem(String emoji, java.util.List<java.lang.String> variants);
+ method public String getEmoji();
+ method public java.util.List<java.lang.String> getVariants();
+ property public final String emoji;
+ property public final java.util.List<java.lang.String> variants;
+ }
+
+ public interface RecentEmojiAsyncProvider {
+ method public com.google.common.util.concurrent.ListenableFuture<java.util.List<java.lang.String>> getRecentEmojiListAsync();
+ method public void recordSelection(String emoji);
+ }
+
+ public interface RecentEmojiProvider {
+ method public suspend Object? getRecentEmojiList(kotlin.coroutines.Continuation<? super java.util.List<java.lang.String>>);
+ method public void recordSelection(String emoji);
+ }
+
+ public final class RecentEmojiProviderAdapter implements androidx.emoji2.emojipicker.RecentEmojiProvider {
+ ctor public RecentEmojiProviderAdapter(androidx.emoji2.emojipicker.RecentEmojiAsyncProvider recentEmojiAsyncProvider);
+ method public suspend Object? getRecentEmojiList(kotlin.coroutines.Continuation<? super java.util.List<java.lang.String>>);
+ method public void recordSelection(String emoji);
+ }
+
+}
+
diff --git a/emoji2/emoji2-views-helper/api/1.5.0-beta01.txt b/emoji2/emoji2-views-helper/api/1.5.0-beta01.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/1.5.0-beta01.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+ public final class EmojiEditTextHelper {
+ ctor public EmojiEditTextHelper(android.widget.EditText);
+ ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+ method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+ method public int getMaxEmojiCount();
+ method public boolean isEnabled();
+ method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+ method public void setEnabled(boolean);
+ method public void setMaxEmojiCount(@IntRange(from=0) int);
+ }
+
+ public final class EmojiTextViewHelper {
+ ctor public EmojiTextViewHelper(android.widget.TextView);
+ ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+ method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+ method public boolean isEnabled();
+ method public void setAllCaps(boolean);
+ method public void setEnabled(boolean);
+ method public void updateTransformationMethod();
+ method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+ }
+
+}
+
diff --git a/emoji2/emoji2-views-helper/api/res-1.5.0-beta01.txt b/emoji2/emoji2-views-helper/api/res-1.5.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/res-1.5.0-beta01.txt
diff --git a/emoji2/emoji2-views-helper/api/restricted_1.5.0-beta01.txt b/emoji2/emoji2-views-helper/api/restricted_1.5.0-beta01.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/restricted_1.5.0-beta01.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+ public final class EmojiEditTextHelper {
+ ctor public EmojiEditTextHelper(android.widget.EditText);
+ ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+ method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+ method public int getMaxEmojiCount();
+ method public boolean isEnabled();
+ method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+ method public void setEnabled(boolean);
+ method public void setMaxEmojiCount(@IntRange(from=0) int);
+ }
+
+ public final class EmojiTextViewHelper {
+ ctor public EmojiTextViewHelper(android.widget.TextView);
+ ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+ method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+ method public boolean isEnabled();
+ method public void setAllCaps(boolean);
+ method public void setEnabled(boolean);
+ method public void updateTransformationMethod();
+ method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+ }
+
+}
+
diff --git a/emoji2/emoji2-views/api/1.5.0-beta01.txt b/emoji2/emoji2-views/api/1.5.0-beta01.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/1.5.0-beta01.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+ public class EmojiButton extends android.widget.Button {
+ ctor public EmojiButton(android.content.Context);
+ ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+ }
+
+ public class EmojiEditText extends android.widget.EditText {
+ ctor public EmojiEditText(android.content.Context);
+ ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+ method public int getMaxEmojiCount();
+ method public void setMaxEmojiCount(@IntRange(from=0) int);
+ }
+
+ public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+ ctor public EmojiExtractTextLayout(android.content.Context);
+ ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+ method public int getEmojiReplaceStrategy();
+ method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+ method public void setEmojiReplaceStrategy(int);
+ }
+
+ public class EmojiTextView extends android.widget.TextView {
+ ctor public EmojiTextView(android.content.Context);
+ ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+ }
+
+}
+
diff --git a/emoji2/emoji2-views/api/res-1.5.0-beta01.txt b/emoji2/emoji2-views/api/res-1.5.0-beta01.txt
new file mode 100644
index 0000000..8bc8423
--- /dev/null
+++ b/emoji2/emoji2-views/api/res-1.5.0-beta01.txt
@@ -0,0 +1,2 @@
+attr emojiReplaceStrategy
+attr maxEmojiCount
diff --git a/emoji2/emoji2-views/api/restricted_1.5.0-beta01.txt b/emoji2/emoji2-views/api/restricted_1.5.0-beta01.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/restricted_1.5.0-beta01.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+ public class EmojiButton extends android.widget.Button {
+ ctor public EmojiButton(android.content.Context);
+ ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+ }
+
+ public class EmojiEditText extends android.widget.EditText {
+ ctor public EmojiEditText(android.content.Context);
+ ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+ method public int getMaxEmojiCount();
+ method public void setMaxEmojiCount(@IntRange(from=0) int);
+ }
+
+ public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+ ctor public EmojiExtractTextLayout(android.content.Context);
+ ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+ method public int getEmojiReplaceStrategy();
+ method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+ method public void setEmojiReplaceStrategy(int);
+ }
+
+ public class EmojiTextView extends android.widget.TextView {
+ ctor public EmojiTextView(android.content.Context);
+ ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+ }
+
+}
+
diff --git a/emoji2/emoji2/api/1.5.0-beta01.txt b/emoji2/emoji2/api/1.5.0-beta01.txt
new file mode 100644
index 0000000..35a7659
--- /dev/null
+++ b/emoji2/emoji2/api/1.5.0-beta01.txt
@@ -0,0 +1,133 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+ public final class DefaultEmojiCompatConfig {
+ method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+ }
+
+ @AnyThread public class EmojiCompat {
+ method public static androidx.emoji2.text.EmojiCompat get();
+ method public String getAssetSignature();
+ method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+ method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+ method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+ method public int getLoadState();
+ method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+ method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+ method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+ method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+ method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+ method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+ method public static boolean isConfigured();
+ method public void load();
+ method @CheckResult public CharSequence? process(CharSequence?);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+ method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void registerInitCallback(java.util.concurrent.Executor, androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+ field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+ field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+ field public static final int EMOJI_FALLBACK = 2; // 0x2
+ field public static final int EMOJI_SUPPORTED = 1; // 0x1
+ field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+ field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+ field public static final int LOAD_STATE_FAILED = 2; // 0x2
+ field public static final int LOAD_STATE_LOADING = 0; // 0x0
+ field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+ field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+ field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+ field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+ field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+ field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+ }
+
+ public abstract static class EmojiCompat.Config {
+ ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+ method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+ method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(java.util.concurrent.Executor, androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+ method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+ method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+ method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+ method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+ method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ }
+
+ public static interface EmojiCompat.GlyphChecker {
+ method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+ }
+
+ public abstract static class EmojiCompat.InitCallback {
+ ctor public EmojiCompat.InitCallback();
+ method public void onFailed(Throwable?);
+ method public void onInitialized();
+ }
+
+ public static interface EmojiCompat.MetadataRepoLoader {
+ method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+ }
+
+ public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+ ctor public EmojiCompat.MetadataRepoLoaderCallback();
+ method public abstract void onFailed(Throwable?);
+ method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+ }
+
+ public static interface EmojiCompat.SpanFactory {
+ method public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+ }
+
+ public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean!> {
+ ctor public EmojiCompatInitializer();
+ method public Boolean create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<? extends java.lang.Object!>!>!> dependencies();
+ }
+
+ public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+ method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+ method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+ }
+
+ public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+ ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+ method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+ method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+ method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+ }
+
+ public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+ method public long getRetryDelay();
+ }
+
+ public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+ method public abstract long getRetryDelay();
+ }
+
+ @AnyThread public final class MetadataRepo {
+ method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+ method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+ method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+ }
+
+ @AnyThread public class TypefaceEmojiRasterizer {
+ method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+ method public int getCodepointAt(int);
+ method public int getCodepointsLength();
+ method public int getHeight();
+ method public android.graphics.Typeface getTypeface();
+ method public int getWidth();
+ method public boolean isDefaultEmoji();
+ method public boolean isPreferredSystemRender();
+ }
+
+}
+
diff --git a/emoji2/emoji2/api/res-1.5.0-beta01.txt b/emoji2/emoji2/api/res-1.5.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2/api/res-1.5.0-beta01.txt
diff --git a/emoji2/emoji2/api/restricted_1.5.0-beta01.txt b/emoji2/emoji2/api/restricted_1.5.0-beta01.txt
new file mode 100644
index 0000000..35a7659
--- /dev/null
+++ b/emoji2/emoji2/api/restricted_1.5.0-beta01.txt
@@ -0,0 +1,133 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+ public final class DefaultEmojiCompatConfig {
+ method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+ }
+
+ @AnyThread public class EmojiCompat {
+ method public static androidx.emoji2.text.EmojiCompat get();
+ method public String getAssetSignature();
+ method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+ method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+ method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+ method public int getLoadState();
+ method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+ method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+ method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+ method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+ method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+ method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+ method public static boolean isConfigured();
+ method public void load();
+ method @CheckResult public CharSequence? process(CharSequence?);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+ method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void registerInitCallback(java.util.concurrent.Executor, androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+ field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+ field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+ field public static final int EMOJI_FALLBACK = 2; // 0x2
+ field public static final int EMOJI_SUPPORTED = 1; // 0x1
+ field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+ field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+ field public static final int LOAD_STATE_FAILED = 2; // 0x2
+ field public static final int LOAD_STATE_LOADING = 0; // 0x0
+ field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+ field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+ field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+ field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+ field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+ field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+ }
+
+ public abstract static class EmojiCompat.Config {
+ ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+ method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+ method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(java.util.concurrent.Executor, androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+ method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+ method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+ method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+ method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+ method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ }
+
+ public static interface EmojiCompat.GlyphChecker {
+ method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+ }
+
+ public abstract static class EmojiCompat.InitCallback {
+ ctor public EmojiCompat.InitCallback();
+ method public void onFailed(Throwable?);
+ method public void onInitialized();
+ }
+
+ public static interface EmojiCompat.MetadataRepoLoader {
+ method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+ }
+
+ public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+ ctor public EmojiCompat.MetadataRepoLoaderCallback();
+ method public abstract void onFailed(Throwable?);
+ method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+ }
+
+ public static interface EmojiCompat.SpanFactory {
+ method public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+ }
+
+ public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean!> {
+ ctor public EmojiCompatInitializer();
+ method public Boolean create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<? extends java.lang.Object!>!>!> dependencies();
+ }
+
+ public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+ method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+ method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+ }
+
+ public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+ ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+ method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+ method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+ method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+ }
+
+ public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+ method public long getRetryDelay();
+ }
+
+ public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+ method public abstract long getRetryDelay();
+ }
+
+ @AnyThread public final class MetadataRepo {
+ method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+ method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+ method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+ }
+
+ @AnyThread public class TypefaceEmojiRasterizer {
+ method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+ method public int getCodepointAt(int);
+ method public int getCodepointsLength();
+ method public int getHeight();
+ method public android.graphics.Typeface getTypeface();
+ method public int getWidth();
+ method public boolean isDefaultEmoji();
+ method public boolean isPreferredSystemRender();
+ }
+
+}
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 728ebc2..5e7a023 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -12,17 +12,18 @@
# -----------------------------------------------------------------------------
androidLintMin = "31.1.0"
-androidxTestRunner = "1.6.0"
-androidxTestRules = "1.6.0"
-androidxTestMonitor = "1.7.0"
-androidxTestCore = "1.6.0"
-androidxTestExtJunit = "1.2.0"
+androidxTestRunner = "1.6.1"
+androidxTestRules = "1.6.1"
+androidxTestMonitor = "1.7.1"
+androidxTestCore = "1.6.1"
+androidxTestExtJunit = "1.2.1"
androidxTestExtTruth = "1.6.0"
annotationVersion = "1.7.0"
atomicFu = "0.17.0"
autoService = "1.0-rc6"
autoValue = "1.6.3"
binaryCompatibilityValidator = "0.15.0-Beta.2"
+builder = "8.6.0-alpha05"
byteBuddy = "1.14.9"
asm = "9.7"
cmake = "3.22.1"
@@ -31,8 +32,8 @@
dependencyAnalysisGradlePlugin = "1.32.0"
dexmaker = "2.28.3"
dokka = "1.8.20-dev-214"
-espresso = "3.6.0"
-espressoDevice = "1.0.0"
+espresso = "3.6.1"
+espressoDevice = "1.0.1"
grpc = "1.52.0"
guavaJre = "33.2.1-jre"
hamcrestCore = "1.3"
@@ -108,6 +109,7 @@
asmCommons = { module = "org.ow2.asm:asm-commons", version.ref = "asm"}
jetbrainsBinaryCompatibilityValidator = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" }
binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin", version.ref = "binaryCompatibilityValidator"}
+builder = { module = "com.android.tools.build:builder", version.ref = "builder" }
byteBuddy = { module = "net.bytebuddy:byte-buddy", version.ref = "byteBuddy" }
byteBuddyAgent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "byteBuddy" }
checkerframework = { module = "org.checkerframework:checker-qual", version = "2.5.3" }
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index f01a72a..6f9a11e 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -1,6 +1,9 @@
// Signature format: 4.0
package androidx.health.connect.client {
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="This is an experimental Health Connect API and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalHealthConnectApi {
+ }
+
@kotlin.jvm.JvmDefaultWithCompatibility public interface HealthConnectClient {
method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 9c0d728..829b64ab 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -1,6 +1,9 @@
// Signature format: 4.0
package androidx.health.connect.client {
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="This is an experimental Health Connect API and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalHealthConnectApi {
+ }
+
@kotlin.jvm.JvmDefaultWithCompatibility public interface HealthConnectClient {
method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
diff --git a/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/ExperimentalHealthConnectApi.kt
similarity index 70%
copy from compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
copy to health/connect/connect-client/src/main/java/androidx/health/connect/client/ExperimentalHealthConnectApi.kt
index d4a754b..fd76bb6 100644
--- a/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/ExperimentalHealthConnectApi.kt
@@ -14,8 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.material3.adaptive.layout
+package androidx.health.connect.client
-import androidx.compose.ui.Modifier
-
-internal actual fun Modifier.systemGestureExclusion(): Modifier = this
+@RequiresOptIn(
+ message = "This is an experimental Health Connect API and is likely to change in the future."
+)
+@Retention(AnnotationRetention.BINARY)
+public annotation class ExperimentalHealthConnectApi
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/changes/DeletionChange.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/changes/DeletionChange.kt
index 222b01b..4a6683a 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/changes/DeletionChange.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/changes/DeletionChange.kt
@@ -15,6 +15,7 @@
*/
package androidx.health.connect.client.changes
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.Record
import androidx.health.connect.client.records.metadata.Metadata
@@ -28,7 +29,9 @@
*
* @property recordId [Metadata.id] of deleted [Record].
*/
-class DeletionChange internal constructor(public val recordId: String) : Change {
+class DeletionChange
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+constructor(public val recordId: String) : Change {
/*
* Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/changes/UpsertionChange.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/changes/UpsertionChange.kt
index a36ac4a..3ad73fa 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/changes/UpsertionChange.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/changes/UpsertionChange.kt
@@ -15,6 +15,7 @@
*/
package androidx.health.connect.client.changes
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.Record
/**
@@ -22,7 +23,9 @@
*
* @property record Updated or inserted record.
*/
-class UpsertionChange internal constructor(public val record: Record) : Change {
+class UpsertionChange
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+constructor(public val record: Record) : Change {
/*
* Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
index bc4c2c3..97bb194 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
@@ -64,18 +64,7 @@
class HealthConnectClientImpl
internal constructor(
private val delegate: HealthDataAsyncClient,
- private val allPermissions: List<String> = buildList {
- addAll(
- HealthPermission.RECORD_TYPE_TO_PERMISSION.flatMap {
- listOf(
- HealthPermission.WRITE_PERMISSION_PREFIX + it.value,
- HealthPermission.READ_PERMISSION_PREFIX + it.value
- )
- }
- )
- add(HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE)
- add(HealthPermission.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND)
- },
+ private val allPermissions: List<String> = HealthPermission.ALL_PERMISSIONS,
) : HealthConnectClient, PermissionController {
override suspend fun getGrantedPermissions(): Set<String> {
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
index f87c7b0..e460b00 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
package androidx.health.connect.client.impl.converters.records
@@ -36,28 +36,28 @@
/** Internal helper functions to convert proto to records. */
@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
-internal val DataProto.DataPoint.startTime: Instant
+val DataProto.DataPoint.startTime: Instant
get() = Instant.ofEpochMilli(startTimeMillis)
@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
-internal val DataProto.DataPoint.endTime: Instant
+val DataProto.DataPoint.endTime: Instant
get() = Instant.ofEpochMilli(endTimeMillis)
@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
-internal val DataProto.DataPoint.time: Instant
+val DataProto.DataPoint.time: Instant
get() = Instant.ofEpochMilli(instantTimeMillis)
@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
-internal val DataProto.DataPoint.startZoneOffset: ZoneOffset?
+val DataProto.DataPoint.startZoneOffset: ZoneOffset?
get() =
if (hasStartZoneOffsetSeconds()) ZoneOffset.ofTotalSeconds(startZoneOffsetSeconds) else null
@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
-internal val DataProto.DataPoint.endZoneOffset: ZoneOffset?
+val DataProto.DataPoint.endZoneOffset: ZoneOffset?
get() = if (hasEndZoneOffsetSeconds()) ZoneOffset.ofTotalSeconds(endZoneOffsetSeconds) else null
@get:SuppressWarnings("GoodTime") // HealthDataClientImplSafe to use for deserialization
-internal val DataProto.DataPoint.zoneOffset: ZoneOffset?
+val DataProto.DataPoint.zoneOffset: ZoneOffset?
get() = if (hasZoneOffsetSeconds()) ZoneOffset.ofTotalSeconds(zoneOffsetSeconds) else null
internal fun DataPointOrBuilder.getLong(key: String, defaultVal: Long = 0): Long =
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
index 93d5287..a63bb6d 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
@@ -348,6 +348,23 @@
WheelchairPushesRecord::class to
READ_WHEELCHAIR_PUSHES.substringAfter(READ_PERMISSION_PREFIX),
)
+
+ /**
+ * Exposes all write and read permissions.
+ *
+ * @return A list of permissions as Strings
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmField
+ public val ALL_PERMISSIONS: List<String> = buildList {
+ addAll(
+ RECORD_TYPE_TO_PERMISSION.flatMap {
+ listOf(WRITE_PERMISSION_PREFIX + it.value, READ_PERMISSION_PREFIX + it.value)
+ }
+ )
+ add(PERMISSION_WRITE_EXERCISE_ROUTE)
+ add(PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND)
+ }
}
override fun equals(other: Any?): Boolean {
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ChangesTokenRequest.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ChangesTokenRequest.kt
index 669e483..0a42c02c 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ChangesTokenRequest.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ChangesTokenRequest.kt
@@ -15,6 +15,7 @@
*/
package androidx.health.connect.client.request
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.Record
import androidx.health.connect.client.records.metadata.DataOrigin
import kotlin.reflect.KClass
@@ -27,8 +28,8 @@
* filter.
*/
class ChangesTokenRequest(
- internal val recordTypes: Set<KClass<out Record>>,
- internal val dataOriginFilters: Set<DataOrigin> = setOf()
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val recordTypes: Set<KClass<out Record>>,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val dataOriginFilters: Set<DataOrigin> = setOf()
) {
/*
* Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt
index ebb1336..c52e70d 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt
@@ -15,6 +15,7 @@
*/
package androidx.health.connect.client.request
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.Record
import androidx.health.connect.client.records.metadata.DataOrigin
import androidx.health.connect.client.time.TimeRangeFilter
@@ -65,12 +66,13 @@
* @see androidx.health.connect.client.HealthConnectClient.readRecords
*/
public class ReadRecordsRequest<T : Record>(
- internal val recordType: KClass<T>,
- internal val timeRangeFilter: TimeRangeFilter,
- internal val dataOriginFilter: Set<DataOrigin> = emptySet(),
- internal val ascendingOrder: Boolean = true,
- internal val pageSize: Int = 1000,
- internal val pageToken: String? = null,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val recordType: KClass<T>,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val timeRangeFilter: TimeRangeFilter,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ val dataOriginFilter: Set<DataOrigin> = emptySet(),
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val ascendingOrder: Boolean = true,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val pageSize: Int = 1000,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val pageToken: String? = null,
) {
init {
require(pageSize > 0) { "pageSize must be positive." }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ChangesResponse.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ChangesResponse.kt
index 205c6ac..b2d38ab 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ChangesResponse.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ChangesResponse.kt
@@ -15,6 +15,7 @@
*/
package androidx.health.connect.client.response
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.changes.Change
/**
@@ -30,7 +31,8 @@
* @see [androidx.health.connect.client.HealthConnectClient.getChanges]
*/
class ChangesResponse
-internal constructor(
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+constructor(
public val changes: List<Change>,
public val nextChangesToken: String,
@get:JvmName("hasMore") public val hasMore: Boolean,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/InsertRecordsResponse.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/InsertRecordsResponse.kt
index aa61efc..5265587 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/InsertRecordsResponse.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/InsertRecordsResponse.kt
@@ -15,13 +15,16 @@
*/
package androidx.health.connect.client.response
+import androidx.annotation.RestrictTo
+
/**
* Response to record insertion.
*
* @see [androidx.health.connect.client.HealthConnectClient.insertRecords]
*/
public class InsertRecordsResponse
-internal constructor(
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+constructor(
/*
* Contains
* [androidx.health.connect.client.metadata.Metadata.recordId] of inserted [Record] in same order as
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ReadRecordResponse.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ReadRecordResponse.kt
index 40afd51..375fc0d 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ReadRecordResponse.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ReadRecordResponse.kt
@@ -15,6 +15,7 @@
*/
package androidx.health.connect.client.response
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.Record
/**
@@ -22,4 +23,6 @@
*
* @see [androidx.health.connect.client.HealthConnectClient.readRecord]
*/
-class ReadRecordResponse<T : Record> internal constructor(val record: T)
+class ReadRecordResponse<T : Record>
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+constructor(val record: T)
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ReadRecordsResponse.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ReadRecordsResponse.kt
index e7e713e..6f51694 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ReadRecordsResponse.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/response/ReadRecordsResponse.kt
@@ -15,6 +15,7 @@
*/
package androidx.health.connect.client.response
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.Record
/**
@@ -28,4 +29,5 @@
* @see androidx.health.connect.client.HealthConnectClient.readRecords
*/
class ReadRecordsResponse<T : Record>
-internal constructor(val records: List<T>, val pageToken: String?)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+constructor(val records: List<T>, val pageToken: String?)
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/time/TimeRangeFilter.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/time/TimeRangeFilter.kt
index c074c2a..864e010 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/time/TimeRangeFilter.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/time/TimeRangeFilter.kt
@@ -15,6 +15,7 @@
*/
package androidx.health.connect.client.time
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.Record
import java.time.Instant
import java.time.LocalDateTime
@@ -34,11 +35,12 @@
* zoneOffset will assume the current system zone offset at query time.
*/
class TimeRangeFilter
-internal constructor(
- internal val startTime: Instant? = null,
- internal val endTime: Instant? = null,
- internal val localStartTime: LocalDateTime? = null,
- internal val localEndTime: LocalDateTime? = null,
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+constructor(
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val startTime: Instant? = null,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val endTime: Instant? = null,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val localStartTime: LocalDateTime? = null,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val localEndTime: LocalDateTime? = null,
) {
companion object {
/**
diff --git a/health/connect/connect-testing/api/current.txt b/health/connect/connect-testing/api/current.txt
index e6f50d0..0183341 100644
--- a/health/connect/connect-testing/api/current.txt
+++ b/health/connect/connect-testing/api/current.txt
@@ -1 +1,41 @@
// Signature format: 4.0
+package androidx.health.connect.client.testing {
+
+ @SuppressCompatibility @androidx.health.connect.client.ExperimentalHealthConnectApi public final class FakeHealthConnectClient implements androidx.health.connect.client.HealthConnectClient {
+ ctor public FakeHealthConnectClient(optional String packageName, optional java.time.Clock clock, optional androidx.health.connect.client.PermissionController permissionController);
+ method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
+ method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
+ method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
+ method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> recordIdsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public void expireToken(String token);
+ method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
+ method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
+ method public int getPageSizeGetChanges();
+ method public androidx.health.connect.client.PermissionController getPermissionController();
+ method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
+ method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
+ method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
+ method public void setPageSizeGetChanges(int);
+ method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final int pageSizeGetChanges;
+ property public androidx.health.connect.client.PermissionController permissionController;
+ field public static final androidx.health.connect.client.testing.FakeHealthConnectClient.Companion Companion;
+ field public static final String DEFAULT_PACKAGE_NAME = "androidx.health.connect.test";
+ }
+
+ public static final class FakeHealthConnectClient.Companion {
+ }
+
+ @SuppressCompatibility @androidx.health.connect.client.ExperimentalHealthConnectApi public final class FakePermissionController implements androidx.health.connect.client.PermissionController {
+ ctor public FakePermissionController(optional boolean grantAll);
+ method public suspend Object? getGrantedPermissions(kotlin.coroutines.Continuation<? super java.util.Set<? extends java.lang.String>>);
+ method public void grantPermission(String permission);
+ method public void grantPermissions(java.util.Set<java.lang.String> permission);
+ method public void replaceGrantedPermissions(java.util.Set<java.lang.String> permissions);
+ method public suspend Object? revokeAllPermissions(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public void revokePermission(String permission);
+ }
+
+}
+
diff --git a/health/connect/connect-testing/api/restricted_current.txt b/health/connect/connect-testing/api/restricted_current.txt
index e6f50d0..0183341 100644
--- a/health/connect/connect-testing/api/restricted_current.txt
+++ b/health/connect/connect-testing/api/restricted_current.txt
@@ -1 +1,41 @@
// Signature format: 4.0
+package androidx.health.connect.client.testing {
+
+ @SuppressCompatibility @androidx.health.connect.client.ExperimentalHealthConnectApi public final class FakeHealthConnectClient implements androidx.health.connect.client.HealthConnectClient {
+ ctor public FakeHealthConnectClient(optional String packageName, optional java.time.Clock clock, optional androidx.health.connect.client.PermissionController permissionController);
+ method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
+ method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
+ method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
+ method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> recordIdsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public void expireToken(String token);
+ method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
+ method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
+ method public int getPageSizeGetChanges();
+ method public androidx.health.connect.client.PermissionController getPermissionController();
+ method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
+ method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
+ method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
+ method public void setPageSizeGetChanges(int);
+ method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final int pageSizeGetChanges;
+ property public androidx.health.connect.client.PermissionController permissionController;
+ field public static final androidx.health.connect.client.testing.FakeHealthConnectClient.Companion Companion;
+ field public static final String DEFAULT_PACKAGE_NAME = "androidx.health.connect.test";
+ }
+
+ public static final class FakeHealthConnectClient.Companion {
+ }
+
+ @SuppressCompatibility @androidx.health.connect.client.ExperimentalHealthConnectApi public final class FakePermissionController implements androidx.health.connect.client.PermissionController {
+ ctor public FakePermissionController(optional boolean grantAll);
+ method public suspend Object? getGrantedPermissions(kotlin.coroutines.Continuation<? super java.util.Set<? extends java.lang.String>>);
+ method public void grantPermission(String permission);
+ method public void grantPermissions(java.util.Set<java.lang.String> permission);
+ method public void replaceGrantedPermissions(java.util.Set<java.lang.String> permissions);
+ method public suspend Object? revokeAllPermissions(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public void revokePermission(String permission);
+ }
+
+}
+
diff --git a/health/connect/connect-testing/build.gradle b/health/connect/connect-testing/build.gradle
index 3d2bacd..57c2591 100644
--- a/health/connect/connect-testing/build.gradle
+++ b/health/connect/connect-testing/build.gradle
@@ -22,6 +22,7 @@
* modifying its settings.
*/
import androidx.build.LibraryType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -31,11 +32,23 @@
dependencies {
api(libs.kotlinStdlib)
- // Add dependencies here
+ implementation project(':health:connect:connect-client')
+ implementation(project(":health:connect:connect-client-proto"))
+
+ testImplementation(libs.kotlinTest)
+ testImplementation(libs.junit)
+ testImplementation(libs.truth)
+ testImplementation(libs.kotlinCoroutinesTest)
}
android {
+ defaultConfig {
+ minSdkVersion 26
+ }
namespace "androidx.health.connect.testing"
+ testOptions.unitTests.includeAndroidResources = true
+ compileSdk = 34
+ compileSdkExtension = 10
}
androidx {
@@ -43,5 +56,11 @@
mavenVersion = LibraryVersions.HEALTH_CONNECT_TESTING_QUARANTINE
type = LibraryType.PUBLISHED_TEST_LIBRARY
inceptionYear = "2024"
- description = "Test HealthConnect by providing a fake HealthConnectClient. This library should be added as a test dependency when writting unit tests that call HealthConnect APIs."
+ description = "Test utils for Health Connect. This library should be added as a test dependency when writing unit tests that call HealthConnect APIs."
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-opt-in=androidx.health.connect.client.ExperimentalHealthConnectApi"]
+ }
}
diff --git a/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/FakeHealthConnectClient.kt b/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/FakeHealthConnectClient.kt
new file mode 100644
index 0000000..3b5ac8d
--- /dev/null
+++ b/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/FakeHealthConnectClient.kt
@@ -0,0 +1,376 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.testing
+
+import androidx.health.connect.client.ExperimentalHealthConnectApi
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.PermissionController
+import androidx.health.connect.client.aggregate.AggregationResult
+import androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration
+import androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod
+import androidx.health.connect.client.changes.Change
+import androidx.health.connect.client.changes.DeletionChange
+import androidx.health.connect.client.changes.UpsertionChange
+import androidx.health.connect.client.impl.converters.datatype.RECORDS_TYPE_NAME_MAP
+import androidx.health.connect.client.impl.converters.records.toProto
+import androidx.health.connect.client.impl.converters.records.toRecord
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.request.AggregateGroupByDurationRequest
+import androidx.health.connect.client.request.AggregateGroupByPeriodRequest
+import androidx.health.connect.client.request.AggregateRequest
+import androidx.health.connect.client.request.ChangesTokenRequest
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.response.ChangesResponse
+import androidx.health.connect.client.response.InsertRecordsResponse
+import androidx.health.connect.client.response.ReadRecordResponse
+import androidx.health.connect.client.response.ReadRecordsResponse
+import androidx.health.connect.client.time.TimeRangeFilter
+import java.time.Clock
+import kotlin.reflect.KClass
+
+/**
+ * Fake [HealthConnectClient] to be used in tests for components that use it as a dependency.
+ *
+ * Features:
+ * * Add, remove, delete and read records using an in-memory object, supporting pagination.
+ * * Token generation and change tracking.
+ *
+ * Note that this fake does not check for permissions.
+ *
+ * @param packageName the name of the package to use to generate unique record IDs.
+ * @param clock used to close open-ended [TimeRangeFilter]s and record update times.
+ * @param permissionController grants and revokes permissions.
+ */
+@ExperimentalHealthConnectApi
+public class FakeHealthConnectClient(
+ private val packageName: String = DEFAULT_PACKAGE_NAME,
+ private val clock: Clock = Clock.systemDefaultZone(),
+ override val permissionController: PermissionController = FakePermissionController()
+) : HealthConnectClient {
+
+ private val idsToRecords: MutableMap<String, Record> = mutableMapOf()
+ private val deletedIdsToRecords: MutableMap<String, Record> = mutableMapOf()
+
+ // Changes are tracked with a map of changes. The key is incremented with each change.
+ private val tokens = mutableMapOf<String, TokenInfo>()
+ private val timeToChanges: MutableMap<Long, Change> = mutableMapOf()
+ private var timeToChangesLastKey = 0L
+
+ private var idCounter = 0
+
+ /**
+ * Overrides the page size to test pagination when calling [getChanges].
+ *
+ * This is typically used with a low number (such as 2) so that a low number of inserted records
+ * (such as 3) generate multiple pages. Use it to test token expiration as well.
+ */
+ public var pageSizeGetChanges: Int = 1000
+
+ /**
+ * Fake implementation that inserts one or more [Record]s into the in-memory store.
+ *
+ * Supports deduplication of
+ * [androidx.health.connect.client.records.metadata.Metadata.clientRecordId]s using
+ * [androidx.health.connect.client.records.metadata.Metadata.clientRecordVersion] to determine
+ * precedence.
+ */
+ override suspend fun insertRecords(records: List<Record>): InsertRecordsResponse {
+ val recordIdsList = mutableListOf<String>()
+ records.forEach { record ->
+ val recordId =
+ record.metadata.clientRecordId?.toRecordId(packageName) ?: "testHCid${idCounter++}"
+ recordIdsList += recordId
+ val insertedRecord =
+ toRecord(
+ record
+ .toProto()
+ .toBuilder()
+ .setUid(recordId)
+ .setUpdateTimeMillis(clock.millis())
+ .build()
+ )
+ // If the recordId exists and the existing clientRecordVersion is higher, don't insert.
+ val newClientRecordVersion = insertedRecord.metadata.clientRecordVersion
+ val existingClientRecordVersion =
+ idsToRecords[recordId]?.metadata?.clientRecordVersion ?: -1
+ if (newClientRecordVersion >= existingClientRecordVersion) {
+ idsToRecords[recordId] = insertedRecord
+ addUpsertionChange(insertedRecord)
+ }
+ }
+ return InsertRecordsResponse(recordIdsList)
+ }
+
+ override suspend fun updateRecords(records: List<Record>) {
+ // Check if all records belong to the package
+ if (records.any { it.packageName != packageName }) {
+ throw SecurityException("Trying to delete records owned by another package")
+ }
+
+ // Fake implementation
+ records.forEach { record ->
+ val recordId =
+ record.metadata.clientRecordId?.toRecordId(packageName) ?: record.metadata.id
+
+ val updatedRecord =
+ toRecord(record.toProto().toBuilder().setUpdateTimeMillis(clock.millis()).build())
+ idsToRecords[recordId] = updatedRecord
+ removeUpsertion(recordId)
+ addUpsertionChange(updatedRecord)
+ }
+ }
+
+ override suspend fun deleteRecords(
+ recordType: KClass<out Record>,
+ recordIdsList: List<String>,
+ clientRecordIdsList: List<String>
+ ) {
+ // Check if all records belong to the package in recordsIdsList
+ if (
+ recordIdsList
+ .asSequence()
+ .mapNotNull { idsToRecords[it]?.packageName }
+ .any { it != packageName }
+ ) {
+ throw SecurityException("Trying to delete records owned by another package")
+ }
+
+ // Check if all records belong to the package in clientRecordIdsList
+ if (
+ clientRecordIdsList
+ .asSequence()
+ .mapNotNull { idsToRecords[it.toRecordId(packageName)]?.packageName }
+ .any { it != packageName }
+ ) {
+ throw SecurityException("Trying to delete records owned by another package")
+ }
+
+ // Fake implementation
+ recordIdsList.forEach { recordId ->
+ idsToRecords[recordId]?.let { deletedIdsToRecords[recordId] = it }
+ idsToRecords.remove(recordId)
+ removeUpsertion(recordId)
+ addDeletionChange(recordId)
+ }
+ clientRecordIdsList.forEach {
+ val recordId = it.toRecordId(packageName)
+ idsToRecords[recordId]?.let { deletedIdsToRecords[recordId] = it }
+ idsToRecords.remove(recordId)
+ addDeletionChange(recordId)
+ }
+ }
+
+ override suspend fun deleteRecords(
+ recordType: KClass<out Record>,
+ timeRangeFilter: TimeRangeFilter
+ ) {
+ val recordIdsToRemove =
+ idsToRecords
+ .filterValues { record ->
+ record::class == recordType && record.isWithin(timeRangeFilter, clock)
+ }
+ .keys
+ for (recordId in recordIdsToRemove) {
+ idsToRecords[recordId]?.let { deletedIdsToRecords[recordId] = it }
+ idsToRecords.remove(recordId)
+ removeUpsertion(recordId)
+ addDeletionChange(recordId)
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override suspend fun <T : Record> readRecord(
+ recordType: KClass<T>,
+ recordId: String
+ ): ReadRecordResponse<T> {
+ return ReadRecordResponse(idsToRecords[recordId.toRecordId(packageName)] as T)
+ }
+
+ /**
+ * Returns records that match the attributes in a [ReadRecordsRequest].
+ *
+ * Features a simple paging implementation. Records must not be updated in between calls.
+ *
+ * @throws IllegalStateException if paging is requested.
+ */
+ @Suppress("UNCHECKED_CAST")
+ public override suspend fun <T : Record> readRecords(
+ request: ReadRecordsRequest<T>
+ ): ReadRecordsResponse<T> {
+ val startIndex = request.pageToken?.toIntOrNull() ?: 0
+ val allRecords =
+ idsToRecords
+ .filterValues { record ->
+ record::class == request.recordType &&
+ record.isWithin(request.timeRangeFilter, clock) &&
+ (request.dataOriginFilter.isEmpty() ||
+ request.dataOriginFilter.contains(record.metadata.dataOrigin))
+ }
+ .values
+ .map { record -> record as T }
+
+ val recordsPending = allRecords.drop(startIndex)
+ val hasMorePages = recordsPending.size > request.pageSize
+
+ // Increment the token if there are more pages
+ val nextPageToken =
+ if (hasMorePages) {
+ // Next page token
+ (request.pageToken?.toLongOrNull() ?: 0) + request.pageSize
+ } else {
+ null
+ }
+ // Fake implementation
+ return ReadRecordsResponse(
+ records = recordsPending.take(request.pageSize),
+ pageToken = nextPageToken?.toString()
+ )
+ }
+
+ /** @throws IllegalStateException if no overrides are configured. */
+ override suspend fun aggregate(request: AggregateRequest): AggregationResult {
+ TODO("This function should be mocked in tests.")
+ }
+
+ /** @throws IllegalStateException if no overrides are configured. */
+ override suspend fun aggregateGroupByDuration(
+ request: AggregateGroupByDurationRequest
+ ): List<AggregationResultGroupedByDuration> {
+ TODO("This function should be mocked in tests.")
+ }
+
+ /** @throws IllegalStateException if no overrides are configured. */
+ override suspend fun aggregateGroupByPeriod(
+ request: AggregateGroupByPeriodRequest
+ ): List<AggregationResultGroupedByPeriod> {
+ TODO("This function should be mocked in tests.")
+ }
+
+ /**
+ * Returns a fake token which contains the key to the next change. Used with [getChanges] to
+ * track changes from the moment this function is called.
+ */
+ override suspend fun getChangesToken(request: ChangesTokenRequest): String {
+ if (request.recordTypes.isEmpty()) {
+ throw IllegalArgumentException("Record types must not be empty")
+ }
+ if (request.dataOriginFilters.isNotEmpty()) {
+ throw UnsupportedOperationException(
+ "Data origin filters are not supported in the " +
+ "fake. Use [StubResponse]s with [overrides.getChangesToken] and " +
+ "[overrides.getChanges] to set a response."
+ )
+ }
+ val nextInstant = timeToChangesLastKey + 1
+ val newToken = generateNewToken(nextInstant, request.recordTypes)
+ tokens[newToken] = TokenInfo(recordTypes = request.recordTypes, time = nextInstant)
+ return newToken
+ }
+
+ /** Set a particular token as expired. This is used to test the response of [getChanges]. */
+ public fun expireToken(token: String) {
+ val tokenInfo = tokens[token] ?: throw IllegalStateException("Token not found")
+ tokens[token] = tokenInfo.copy(expired = true)
+ }
+
+ private fun generateNewToken(time: Long, recordTypes: Set<KClass<out Record>>): String {
+ val recordTypesHash =
+ recordTypes
+ .mapNotNull { record ->
+ // Get a string representation of each record
+ RECORDS_TYPE_NAME_MAP.filterValues { it == record }.keys.firstOrNull()
+ }
+ .sorted() // Sort them alphabetically so that the order doesn't matter
+ .takeIf { it.isNotEmpty() }
+ ?.joinToString(",", prefix = "_") ?: ""
+
+ return "$time$recordTypesHash"
+ }
+
+ override suspend fun getChanges(changesToken: String): ChangesResponse {
+ // The token is related to a moment in time in [tokenToTime] and to a set of Record types
+ // in [tokenToRecordTypes].
+ val tokenInfo = tokens[changesToken] ?: throw IllegalStateException("Token not found")
+ val timeInToken = tokenInfo.time
+ val recordTypes = tokenInfo.recordTypes
+
+ val changes =
+ timeToChanges
+ .filterKeys { key -> key >= timeInToken }
+ .filterValues { change: Change ->
+ // Only return changes whose records are of the requested types
+ when (change) {
+ is UpsertionChange -> recordTypes.contains(change.record::class)
+ is DeletionChange -> {
+ val record = deletedIdsToRecords[change.recordId] ?: false
+ recordTypes.contains(record::class)
+ }
+ else -> throw NotImplementedError()
+ }
+ }
+ .values
+ val hasMoreChanges = changes.size > pageSizeGetChanges
+ val nextChangesToken =
+ if (hasMoreChanges) {
+ // Next page token
+ generateNewToken(timeInToken + pageSizeGetChanges, recordTypes)
+ } else {
+ // Future changes token
+ generateNewToken(timeToChangesLastKey + 1, recordTypes)
+ }
+
+ // Store metadata for new token
+ tokens[nextChangesToken] = tokenInfo.copy(time = tokenInfo.time + pageSizeGetChanges)
+
+ return ChangesResponse(
+ changes.take(pageSizeGetChanges).toList(),
+ hasMore = hasMoreChanges,
+ changesTokenExpired = tokenInfo.expired,
+ nextChangesToken = nextChangesToken
+ )
+ }
+
+ private fun String.toRecordId(packageName: String): String {
+ return "$packageName:$this"
+ }
+
+ private fun addDeletionChange(recordId: String) {
+ timeToChanges[++timeToChangesLastKey] = DeletionChange(recordId)
+ }
+
+ private fun addUpsertionChange(updatedRecord: Record) {
+ timeToChanges[++timeToChangesLastKey] = UpsertionChange(updatedRecord)
+ }
+
+ private fun removeUpsertion(recordId: String) {
+ timeToChanges
+ .filterValues { it is UpsertionChange && it.record.metadata.id == recordId }
+ .keys
+ .forEach { timeToChanges.remove(it) }
+ }
+
+ public companion object {
+ /** Default package name used in [FakeHealthConnectClient]. */
+ public const val DEFAULT_PACKAGE_NAME: String = "androidx.health.connect.test"
+ }
+}
+
+private data class TokenInfo(
+ val time: Long,
+ val recordTypes: Set<KClass<out Record>>,
+ val expired: Boolean = false
+)
diff --git a/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/FakePermissionController.kt b/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/FakePermissionController.kt
new file mode 100644
index 0000000..539f319
--- /dev/null
+++ b/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/FakePermissionController.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.testing
+
+import androidx.health.connect.client.ExperimentalHealthConnectApi
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.PermissionController
+import androidx.health.connect.client.permission.HealthPermission
+
+/**
+ * A fake [PermissionController] that enables full control of permissions in tests for a
+ * [HealthConnectClient].
+ *
+ * @param grantAll grants all permissions on creation
+ */
+@ExperimentalHealthConnectApi
+public class FakePermissionController(grantAll: Boolean = true) : PermissionController {
+ private val grantedPermissions =
+ if (grantAll) HealthPermission.ALL_PERMISSIONS.toMutableSet() else mutableSetOf()
+
+ /** Replaces the set of permissions returned by [getGrantedPermissions] with a new set. */
+ public fun replaceGrantedPermissions(permissions: Set<String>) {
+ grantedPermissions.clear()
+ grantedPermissions.addAll(permissions)
+ }
+
+ /** Adds a permission to the set of granted permissions returned by [getGrantedPermissions]. */
+ public fun grantPermission(permission: String) {
+ grantedPermissions.add(permission)
+ }
+
+ /** Adds permissions to the set of granted permissions returned by [getGrantedPermissions]. */
+ public fun grantPermissions(permission: Set<String>) {
+ grantedPermissions.addAll(permission)
+ }
+
+ /**
+ * Removes a permission from the set of granted permissions returned by [getGrantedPermissions].
+ */
+ public fun revokePermission(permission: String) {
+ grantedPermissions.remove(permission)
+ }
+
+ /** Returns a fake set of permissions. */
+ override suspend fun getGrantedPermissions(): Set<String> {
+ return grantedPermissions.toSet()
+ }
+
+ /** Clears the set of permissions returned by [getGrantedPermissions]. */
+ override suspend fun revokeAllPermissions() {
+ grantedPermissions.clear()
+ }
+}
diff --git a/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/RecordTestUtils.kt b/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/RecordTestUtils.kt
new file mode 100644
index 0000000..3c3dd99
--- /dev/null
+++ b/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/RecordTestUtils.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.testing
+
+import androidx.health.connect.client.impl.converters.records.endTime
+import androidx.health.connect.client.impl.converters.records.startTime
+import androidx.health.connect.client.impl.converters.records.time
+import androidx.health.connect.client.impl.converters.records.toProto
+import androidx.health.connect.client.impl.converters.records.zoneOffset
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.time.TimeRangeFilter
+import androidx.health.platform.client.proto.DataProto
+import java.time.Clock
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+
+internal fun Record.isWithin(filter: TimeRangeFilter, clock: Clock): Boolean {
+ val proto: DataProto.DataPoint = toProto()
+ val timeRangeFilter = filter.sanitize(clock)
+
+ if (timeRangeFilter.isLocalBasedFilter()) {
+ if (proto.hasInstantTimeMillis()) {
+ val time =
+ LocalDateTime.ofInstant(proto.time, proto.zoneOffset ?: ZoneId.systemDefault())
+ return !time.isBefore(timeRangeFilter.localStartTime) &&
+ timeRangeFilter.localEndTime!!.isAfter(time)
+ }
+ val startTime =
+ LocalDateTime.ofInstant(proto.startTime, proto.zoneOffset ?: ZoneId.systemDefault())
+ return !startTime.isBefore(timeRangeFilter.localStartTime) &&
+ timeRangeFilter.localEndTime!!.isAfter(startTime)
+ }
+
+ if (proto.hasInstantTimeMillis()) {
+ return proto.time >= timeRangeFilter.startTime && // Inclusive
+ proto.time.isBefore(timeRangeFilter.endTime) // Exclusive
+ }
+
+ return proto.startTime >= timeRangeFilter.startTime && // Inclusive
+ proto.endTime.isBefore(timeRangeFilter.endTime) // Exclusive
+}
+
+private fun TimeRangeFilter.sanitize(clock: Clock): TimeRangeFilter {
+ if (isLocalBasedFilter()) {
+ return TimeRangeFilter.between(
+ startTime =
+ localStartTime ?: LocalDateTime.ofInstant(Instant.EPOCH, ZoneId.systemDefault()),
+ endTime =
+ localEndTime ?: LocalDateTime.ofInstant(clock.instant(), ZoneId.systemDefault())
+ )
+ }
+ return TimeRangeFilter.between(
+ startTime = startTime ?: Instant.EPOCH,
+ endTime = endTime ?: clock.instant()
+ )
+}
+
+private fun TimeRangeFilter.isLocalBasedFilter(): Boolean {
+ return localStartTime != null || localEndTime != null
+}
+
+/** Gets the package name from metadata */
+internal val Record.packageName: String
+ get() = this.metadata.dataOrigin.packageName
diff --git a/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakeHealthConnectClientChangesTest.kt b/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakeHealthConnectClientChangesTest.kt
new file mode 100644
index 0000000..bf7a5a1
--- /dev/null
+++ b/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakeHealthConnectClientChangesTest.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.testing
+
+import androidx.health.connect.client.changes.DeletionChange
+import androidx.health.connect.client.changes.UpsertionChange
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.request.ChangesTokenRequest
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.testing.testdata.generateRunningRecords
+import androidx.health.connect.client.testing.testdata.hydrationRecord1
+import androidx.health.connect.client.testing.testdata.runRecord1
+import androidx.health.connect.client.time.TimeRangeFilter
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert
+import org.junit.Test
+
+/** Unit tests for functions related to changes in [FakeHealthConnectClient]. */
+class FakeHealthConnectClientChangesTest {
+
+ @Test
+ fun defaultToken_isOne() = runTest {
+ val fake = FakeHealthConnectClient()
+ val token = fake.getChangesToken(ChangesTokenRequest(recordTypes = setOf(Record::class)))
+ assertThat(token).startsWith("1")
+ }
+
+ @Test
+ fun insertAndGetChanges_returnsUpsertionChange() = runTest {
+ val fake = FakeHealthConnectClient()
+
+ val changesToken =
+ fake.getChangesToken(ChangesTokenRequest(recordTypes = setOf(runRecord1::class)))
+ fake.insertRecords(listOf(runRecord1))
+ val changesResponse = fake.getChanges(changesToken)
+
+ assertThat(changesResponse.changes).hasSize(1)
+ assertThat(changesResponse.changes.first()).isInstanceOf(UpsertionChange::class.java)
+ }
+
+ @Test
+ fun insertAndDeleteAndGetChanges_returnsUpsertionAndDeletionChanges() = runTest {
+ val fake = FakeHealthConnectClient()
+
+ val changesToken =
+ fake.getChangesToken(ChangesTokenRequest(recordTypes = setOf(runRecord1::class)))
+ fake.insertRecords(listOf(runRecord1))
+ fake.deleteRecords(
+ runRecord1::class,
+ clientRecordIdsList = listOf(runRecord1.metadata.clientRecordId!!),
+ recordIdsList = emptyList()
+ )
+ val changesResponse = fake.getChanges(changesToken)
+
+ assertThat(changesResponse.changes).hasSize(2)
+ assertThat((changesResponse.changes[0] as UpsertionChange).record.metadata.clientRecordId)
+ .isEqualTo(runRecord1.metadata.clientRecordId)
+ assertThat(changesResponse.changes[1]).isInstanceOf(DeletionChange::class.java)
+ }
+
+ @Test
+ fun insertAndDeleteAndGetChangesByRecordIds_returnsOnlyDeletionChanges() = runTest {
+ val fake = FakeHealthConnectClient()
+
+ val changesToken =
+ fake.getChangesToken(ChangesTokenRequest(recordTypes = setOf(runRecord1::class)))
+ fake.insertRecords(listOf(runRecord1))
+ val recordWithId =
+ fake.readRecords(
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = runRecord1.startTime.minusSeconds(1),
+ endTime = runRecord1.endTime.plusSeconds(1)
+ ),
+ recordType = runRecord1::class
+ )
+ )
+ fake.deleteRecords(
+ runRecord1::class,
+ recordIdsList = listOf(recordWithId.records.first().metadata.id),
+ clientRecordIdsList = emptyList()
+ )
+ val changesResponse = fake.getChanges(changesToken)
+
+ assertThat(changesResponse.changes).hasSize(1)
+ assertThat(changesResponse.changes[0]).isInstanceOf(DeletionChange::class.java)
+ }
+
+ @Test
+ fun pagination_onePage() = runTest {
+ val fake = FakeHealthConnectClient()
+ val records = generateRunningRecords(3)
+
+ val changesToken =
+ fake.getChangesToken(ChangesTokenRequest(recordTypes = setOf(records.first()::class)))
+ fake.insertRecords(records)
+ val changesResponse = fake.getChanges(changesToken)
+
+ assertThat(changesResponse.changes).hasSize(3)
+ assertThat(changesResponse.hasMore).isFalse()
+ assertThat(changesResponse.nextChangesToken).startsWith("4")
+ }
+
+ @Test
+ fun pagination_twoPages() = runTest {
+ val fake = FakeHealthConnectClient().apply { pageSizeGetChanges = 2 }
+ val records = generateRunningRecords(3)
+
+ val changesToken =
+ fake.getChangesToken(ChangesTokenRequest(recordTypes = setOf(records.first()::class)))
+ fake.insertRecords(records)
+ val changesResponse = fake.getChanges(changesToken)
+
+ assertThat(changesResponse.changes).hasSize(2)
+ assertThat(changesResponse.hasMore).isTrue()
+ assertThat(changesResponse.nextChangesToken).startsWith("3")
+ }
+
+ @Test
+ fun pagination_twoPages_secondPageCorrect() = runTest {
+ val fake = FakeHealthConnectClient().apply { pageSizeGetChanges = 2 }
+ val records = generateRunningRecords(3)
+
+ val changesToken =
+ fake.getChangesToken(ChangesTokenRequest(recordTypes = setOf(records.first()::class)))
+ fake.insertRecords(records)
+ val page1 = fake.getChanges(changesToken)
+ val page2 = fake.getChanges(page1.nextChangesToken)
+
+ assertThat(page2.changes).hasSize(1)
+ assertThat(page2.hasMore).isFalse()
+ assertThat(page2.nextChangesToken).startsWith("4")
+ }
+
+ @Test
+ fun getChangesToken_differentFilters_differentResults() = runTest {
+ val fake = FakeHealthConnectClient()
+
+ val changesToken1 =
+ fake.getChangesToken(ChangesTokenRequest(recordTypes = setOf(runRecord1::class)))
+ val changesToken2 =
+ fake.getChangesToken(ChangesTokenRequest(recordTypes = setOf(hydrationRecord1::class)))
+
+ fake.insertRecords(listOf(runRecord1))
+ val changesResponse1 = fake.getChanges(changesToken1)
+ val changesResponse2 = fake.getChanges(changesToken2)
+
+ assertThat(changesResponse1.changes).hasSize(1)
+ assertThat(changesResponse2.changes).hasSize(0)
+ fake.insertRecords(listOf(hydrationRecord1))
+
+ val changesResponse2AfterInsert = fake.getChanges(changesToken2)
+ assertThat(changesResponse2AfterInsert.changes).hasSize(1)
+ }
+
+ @Test
+ fun getChangesTokenExpiration_resultWithExpiredToken() = runTest {
+ val fake = FakeHealthConnectClient()
+ val records = generateRunningRecords(3)
+
+ val changesToken =
+ fake.getChangesToken(ChangesTokenRequest(recordTypes = setOf(runRecord1::class)))
+ fake.expireToken(changesToken)
+ fake.insertRecords(records)
+ val changesResponse = fake.getChanges(changesToken)
+
+ assertThat(changesResponse.changesTokenExpired).isTrue()
+ }
+
+ @Test
+ fun getChangesDefaultTokenExpiration_noExpiredToken() = runTest {
+ val fake = FakeHealthConnectClient()
+ val records = generateRunningRecords(3)
+
+ val changesToken =
+ fake.getChangesToken(ChangesTokenRequest(recordTypes = setOf(records.first()::class)))
+ fake.insertRecords(records)
+ val changesResponse = fake.getChanges(changesToken)
+
+ assertThat(changesResponse.changesTokenExpired).isFalse()
+ }
+
+ @Test
+ fun getChangesToken_noRecordType_throws() = runTest {
+ val fake = FakeHealthConnectClient()
+ Assert.assertThrows(IllegalArgumentException::class.java) {
+ runBlocking { fake.getChangesToken(ChangesTokenRequest(recordTypes = emptySet())) }
+ }
+ }
+
+ @Test
+ fun getChangesToken_dataOriginsSet_throws() = runTest {
+ val fake = FakeHealthConnectClient()
+ Assert.assertThrows(UnsupportedOperationException::class.java) {
+ runBlocking {
+ fake.getChangesToken(
+ ChangesTokenRequest(
+ recordTypes = setOf(Record::class),
+ dataOriginFilters = setOf(DataOrigin("test"))
+ )
+ )
+ }
+ }
+ }
+}
diff --git a/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakeHealthConnectClientClockTest.kt b/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakeHealthConnectClientClockTest.kt
new file mode 100644
index 0000000..c40a364
--- /dev/null
+++ b/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakeHealthConnectClientClockTest.kt
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.testing
+
+import androidx.health.connect.client.records.ExerciseSessionRecord
+import androidx.health.connect.client.records.HeightRecord
+import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.time.TimeRangeFilter
+import androidx.health.connect.client.units.Length
+import com.google.common.truth.Truth.assertThat
+import java.time.Clock
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+/**
+ * Tests for the [Clock] parameter in [FakeHealthConnectClient] which is used for open-ended time
+ * ranges. It unit tests the [isWithin] and [sanitize] functions.
+ */
+class FakeHealthConnectClientClockTest {
+ private val fixedInstant = Instant.parse("2000-01-01T10:00:00Z")
+
+ private val clock = Clock.fixed(fixedInstant, ZoneOffset.UTC)
+ private val zoneOffset = clock.zone.rules.getOffset(fixedInstant)
+
+ private val record1 =
+ ExerciseSessionRecord(
+ startTime = fixedInstant.minusSeconds(60),
+ startZoneOffset = zoneOffset,
+ endTime = fixedInstant.minusSeconds(30),
+ endZoneOffset = zoneOffset,
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
+ title = "Record1",
+ exerciseRoute = null,
+ metadata = Metadata(clientRecordId = "FakeHealthConnectData1")
+ )
+
+ private val record2 =
+ ExerciseSessionRecord(
+ startTime = fixedInstant.minusSeconds(29),
+ startZoneOffset = zoneOffset,
+ endTime = fixedInstant.minusSeconds(1),
+ endZoneOffset = zoneOffset,
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
+ title = "Record2",
+ exerciseRoute = null,
+ metadata = Metadata(clientRecordId = "FakeHealthConnectData2")
+ )
+
+ private val fake =
+ FakeHealthConnectClient(clock = clock).apply {
+ runBlocking { insertRecords(listOf(record1, record2)) }
+ }
+
+ @Test
+ fun timeRangeFilter_openEnded_endTime() = runTest {
+ // Read records that start when record 2 starts
+ val records =
+ fake.readRecords(
+ ReadRecordsRequest(
+ record1::class,
+ // No endTime, defaults to clock
+ timeRangeFilter = TimeRangeFilter(startTime = record2.startTime),
+ )
+ )
+
+ // Only record2 should be returned
+ assertThat(records.records).hasSize(1)
+ assertThat(records.records.first().title).isEqualTo(record2.title)
+ }
+
+ @Test
+ fun timeRangeFilter_openEndedEndTime_clockMiddle() = runTest {
+ // Record 2 is in the future for this clock
+ val clock = Clock.fixed(record1.endTime.plusMillis(1), ZoneOffset.UTC)
+ val fake =
+ FakeHealthConnectClient(clock = clock).apply {
+ runBlocking { insertRecords(listOf(record1, record2)) }
+ }
+ val records =
+ fake.readRecords(
+ ReadRecordsRequest(
+ record1::class,
+ // No endTime, defaults to clock
+ timeRangeFilter = TimeRangeFilter(startTime = record1.startTime.minusMillis(1)),
+ )
+ )
+
+ // Only record1 should be returned
+ assertThat(records.records).hasSize(1)
+ assertThat(records.records.first().title).isEqualTo(record1.title)
+ }
+
+ @Test
+ fun timeRangeFilter_openEndedStartTimeEpoch_endTimeIsEnd() = runTest {
+
+ // Read records that end when record 2 ends
+ val records =
+ fake.readRecords(
+ ReadRecordsRequest(
+ record1::class,
+ // No startTime defaults to EPOCH
+ timeRangeFilter = TimeRangeFilter(endTime = record2.endTime.plusMillis(1)),
+ )
+ )
+
+ // Both records should be returned
+ assertThat(records.records).hasSize(2)
+ }
+
+ @Test
+ fun timeRangeFilter_openEndedStartTimeEpoch_endTimeIsMiddle() = runTest {
+
+ // Read records that end when record 1 ends
+ val records =
+ fake.readRecords(
+ ReadRecordsRequest(
+ record1::class,
+ // No startTime defaults to EPOCH
+ timeRangeFilter = TimeRangeFilter(endTime = record1.endTime.plusMillis(1)),
+ )
+ )
+
+ // Only record1 should be returned
+ assertThat(records.records).hasSize(1)
+ assertThat(records.records.first().title).isEqualTo(record1.title)
+ }
+
+ @Test
+ fun timeRangeFilterlocalTime_noEndTime() = runTest {
+
+ // Read records that end when record 1 ends
+ val records =
+ fake.readRecords(
+ ReadRecordsRequest(
+ record1::class,
+ // No endTime, defaults to clock
+ timeRangeFilter =
+ TimeRangeFilter(
+ localStartTime = LocalDateTime.of(2000, 1, 1, 9, 59, 30, 1)
+ ),
+ )
+ )
+
+ // Only record1 should be returned
+ assertThat(records.records).hasSize(1)
+ assertThat(records.records.first().title).isEqualTo(record2.title)
+ }
+
+ @Test
+ fun timeRangeFilter_noEndTimeInstant() = runTest {
+ // Given a record with a fixed time, before the clock.
+ val heightRecord =
+ HeightRecord(
+ time = fixedInstant.minusSeconds(29),
+ metadata = Metadata(clientRecordId = "HeightRecord#1"),
+ height = Length.meters(1.8),
+ zoneOffset = zoneOffset
+ )
+ val fake =
+ FakeHealthConnectClient(clock = clock).apply {
+ runBlocking { insertRecords(listOf(heightRecord)) }
+ }
+ // Records that start before the record.
+ val recordsIncluding =
+ fake.readRecords(
+ ReadRecordsRequest(
+ heightRecord::class,
+ // No endTime, defaults to clock
+ timeRangeFilter = TimeRangeFilter(startTime = fixedInstant.minusSeconds(30)),
+ )
+ )
+ // Records that start after the record.
+ val recordsExcluding =
+ fake.readRecords(
+ ReadRecordsRequest(
+ heightRecord::class,
+ // No endTime, defaults to clock
+ timeRangeFilter = TimeRangeFilter(startTime = fixedInstant.minusSeconds(1)),
+ )
+ )
+
+ // Only record1 should be returned
+ assertThat(recordsIncluding.records).hasSize(1)
+ assertThat(recordsExcluding.records).hasSize(0)
+ }
+
+ @Test
+ fun timeRangeFilterlocalTime_noEndTimeInstant() = runTest {
+ // Given a record with a fixed time, before the clock.
+ val heightRecord =
+ HeightRecord(
+ time = fixedInstant.minusSeconds(29),
+ metadata = Metadata(clientRecordId = "HeightRecord#1"),
+ height = Length.meters(1.8),
+ zoneOffset = zoneOffset
+ )
+ val fake =
+ FakeHealthConnectClient(clock = clock).apply {
+ runBlocking { insertRecords(listOf(heightRecord)) }
+ }
+ // Records that start before the record.
+ val recordsIncluding =
+ fake.readRecords(
+ ReadRecordsRequest(
+ heightRecord::class,
+ // No endTime, defaults to clock
+ timeRangeFilter =
+ TimeRangeFilter(
+ localStartTime = LocalDateTime.of(2000, 1, 1, 9, 59, 30, 1)
+ ),
+ )
+ )
+ // Records that start after the record.
+ val recordsExcluding =
+ fake.readRecords(
+ ReadRecordsRequest(
+ heightRecord::class,
+ // No endTime, defaults to clock
+ timeRangeFilter =
+ TimeRangeFilter(
+ localStartTime = LocalDateTime.of(2000, 1, 1, 9, 59, 31, 1)
+ ),
+ )
+ )
+
+ // Only record1 should be returned
+ assertThat(recordsIncluding.records).hasSize(1)
+ assertThat(recordsExcluding.records).hasSize(0)
+ }
+
+ @Test
+ fun timeRangeFilterlocalTime_noStartTimeInstant() = runTest {
+ // Given a record with a fixed time, before the clock.
+ val heightRecord =
+ HeightRecord(
+ time = fixedInstant.minusSeconds(29),
+ metadata = Metadata(clientRecordId = "HeightRecord#1"),
+ height = Length.meters(1.8),
+ zoneOffset = zoneOffset
+ )
+ val fake =
+ FakeHealthConnectClient(clock = clock).apply {
+ runBlocking { insertRecords(listOf(heightRecord)) }
+ }
+ // Records that end before the record.
+ val recordsIncluding =
+ fake.readRecords(
+ ReadRecordsRequest(
+ heightRecord::class,
+ // No startTime, defaults to EPOCH
+ timeRangeFilter =
+ TimeRangeFilter(localEndTime = LocalDateTime.of(2000, 1, 1, 9, 59, 30)),
+ )
+ )
+ // Records that end after the record.
+ val recordsExcluding =
+ fake.readRecords(
+ ReadRecordsRequest(
+ heightRecord::class,
+ // No endTime, defaults to clock
+ timeRangeFilter =
+ TimeRangeFilter(localEndTime = LocalDateTime.of(2000, 1, 1, 9, 59, 59)),
+ )
+ )
+
+ // Only record2 should be returned
+ assertThat(recordsIncluding.records).hasSize(0)
+ assertThat(recordsExcluding.records).hasSize(1)
+ }
+}
diff --git a/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakeHealthConnectClientRecordsTest.kt b/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakeHealthConnectClientRecordsTest.kt
new file mode 100644
index 0000000..c1e664a
--- /dev/null
+++ b/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakeHealthConnectClientRecordsTest.kt
@@ -0,0 +1,360 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.testing
+
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.testing.testdata.generateRunningRecords
+import androidx.health.connect.client.testing.testdata.runRecord1
+import androidx.health.connect.client.testing.testdata.runRecord1Updated
+import androidx.health.connect.client.time.TimeRangeFilter
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertThrows
+import org.junit.Test
+
+/** Unit tests for functions related to [Record] in [FakeHealthConnectClient]. */
+class FakeHealthConnectClientRecordsTest {
+
+ @Test
+ fun insertSingleRecordAndRead_responseHasOneRecord() {
+ runTest {
+ val fake = FakeHealthConnectClient()
+ fake.insertRecords(listOf(runRecord1))
+ val response =
+ fake.readRecords(
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = runRecord1.startTime.minusSeconds(1),
+ endTime = runRecord1.endTime.plusSeconds(1)
+ ),
+ recordType = runRecord1::class
+ )
+ )
+ assertThat(response.records).hasSize(1)
+ assertThat(response.records.first().metadata.clientRecordId)
+ .isEqualTo(runRecord1.metadata.clientRecordId)
+ }
+ }
+
+ @Test
+ fun updateRecordAndRead_responseHasUpdated() {
+ // Given
+ val fakeRecord = runRecord1
+ val updatedRecord = runRecord1Updated
+ runTest {
+ val fake = FakeHealthConnectClient()
+ fake.insertRecords(listOf(fakeRecord))
+ fake.updateRecords(listOf(updatedRecord))
+ val responseOldRange =
+ fake.readRecords(
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = fakeRecord.startTime.minusSeconds(1),
+ endTime = fakeRecord.endTime.plusSeconds(1)
+ ),
+ recordType = fakeRecord::class
+ )
+ )
+
+ val responseNewRange =
+ fake.readRecords(
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = updatedRecord.startTime.minusSeconds(1),
+ endTime = updatedRecord.endTime.plusSeconds(1)
+ ),
+ recordType = fakeRecord::class
+ )
+ )
+
+ assertThat(responseOldRange.records).hasSize(0)
+ assertThat(responseNewRange.records).hasSize(1)
+ }
+ }
+
+ @Test
+ fun insertMultipleRecordsAndDeleteclientRecordIds_responseIsEmpty() = runTest {
+ val fake = FakeHealthConnectClient()
+ val records = generateRunningRecords(5)
+ fake.insertRecords(records)
+
+ val clientRecordIds = records.map { it.metadata.clientRecordId!! }
+ fake.deleteRecords(records.first()::class, emptyList(), clientRecordIds)
+
+ val response =
+ fake.readRecords(
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = records.first().startTime.minusSeconds(1),
+ endTime = records.last().endTime.plusSeconds(1)
+ ),
+ recordType = records.first()::class
+ )
+ )
+ assertThat(response.records).isEmpty()
+ }
+
+ @Test
+ fun insertMultipleRecordsAndDeleteRecordIds_responseIsEmpty() = runTest {
+ val fake = FakeHealthConnectClient()
+ val records = generateRunningRecords(5)
+ fake.insertRecords(records)
+
+ val responseBeforeDeletion =
+ fake.readRecords(
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = records.first().startTime.minusSeconds(1),
+ endTime = records.last().endTime.plusSeconds(1)
+ ),
+ recordType = records.first()::class
+ )
+ )
+
+ val recordIds = responseBeforeDeletion.records.map { it.metadata.id }
+ fake.deleteRecords(records.first()::class, recordIds, emptyList())
+
+ val response =
+ fake.readRecords(
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = records.first().startTime.minusSeconds(1),
+ endTime = records.last().endTime.plusSeconds(1)
+ ),
+ recordType = records.first()::class
+ )
+ )
+ assertThat(response.records).isEmpty()
+ }
+
+ @Test
+ fun insertMultipleRecordsAndDeleteRecordByTimeRangeFilter_responseIsEmpty() = runTest {
+ val fake = FakeHealthConnectClient()
+ val records = generateRunningRecords(5)
+ fake.insertRecords(records)
+
+ fake.deleteRecords(
+ recordType = records.first()::class,
+ timeRangeFilter =
+ TimeRangeFilter(records.first().startTime.minusMillis(1), records.first().endTime)
+ )
+
+ val response =
+ fake.readRecords(
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = records.first().startTime.minusSeconds(1),
+ endTime = records.last().endTime.plusSeconds(1)
+ ),
+ recordType = records.first()::class
+ )
+ )
+ // Only one record should be presend
+ assertThat(response.records).hasSize(4)
+ }
+
+ @Test
+ fun insertMultipleRecordsAndReadRecord() = runTest {
+ val fake = FakeHealthConnectClient()
+ val records = generateRunningRecords(5)
+ fake.insertRecords(records)
+
+ val recordToRead = records[1]
+ val response = fake.readRecord(recordToRead::class, recordToRead.metadata.clientRecordId!!)
+
+ assertThat(response.record.metadata.clientRecordId)
+ .isEqualTo(recordToRead.metadata.clientRecordId)
+ }
+
+ @Test
+ fun insertMultipleRecordsAndDeleteFour_responseIsOne() = runTest {
+ val fake = FakeHealthConnectClient()
+ val records = generateRunningRecords(5)
+ fake.insertRecords(records)
+
+ val clientRecordIds = records.drop(1).map { it.metadata.clientRecordId!! }
+ fake.deleteRecords(records.first()::class, emptyList(), clientRecordIds)
+
+ val response =
+ fake.readRecords(
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = records.first().startTime.minusSeconds(1),
+ endTime = records.last().endTime.plusSeconds(1)
+ ),
+ recordType = records.first()::class
+ )
+ )
+ assertThat(response.records).hasSize(1)
+ }
+
+ @Test
+ fun insertMultipleRecords_DeleteDifferentPackageRecordIds_throws() = runTest {
+ val fake = FakeHealthConnectClient(packageName = "com.other.package")
+ val records = generateRunningRecords(5) // Uses default package name
+ fake.insertRecords(records)
+
+ // Need to fetch the records from the API to be able to read their new IDs.
+ val recordsResponse =
+ fake.readRecords(
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = records.first().startTime.minusSeconds(1),
+ endTime = records.last().endTime.plusSeconds(1)
+ ),
+ recordType = records.first()::class
+ )
+ )
+
+ val recordIds = recordsResponse.records.map { it.metadata.id }
+
+ assertThrows(SecurityException::class.java) {
+ runBlocking {
+ fake.deleteRecords(
+ recordType = records.first()::class,
+ recordIdsList = recordIds,
+ clientRecordIdsList = emptyList()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun insertMultipleRecords_DeleteDifferentPackageClientRecords_throws() = runTest {
+ val fake = FakeHealthConnectClient(packageName = "com.other.package")
+ val records = generateRunningRecords(5)
+ fake.insertRecords(records)
+
+ val clientRecordIds = records.map { it.metadata.clientRecordId!! }
+
+ assertThrows(SecurityException::class.java) {
+ runBlocking { fake.deleteRecords(records.first()::class, emptyList(), clientRecordIds) }
+ }
+ }
+
+ @Test
+ fun insertMultipleRecords_UpdateDifferentPackageClientRecords_throws() = runTest {
+ val fake = FakeHealthConnectClient(packageName = "com.other.package")
+ val records = generateRunningRecords(5)
+ fake.insertRecords(records)
+
+ assertThrows(SecurityException::class.java) {
+ runBlocking { fake.updateRecords(listOf(records.first())) }
+ }
+ }
+
+ @Test
+ fun insertMultipleRecords_UpdateNonExistingClientRecords_doesNotThrow() = runTest {
+ val fake = FakeHealthConnectClient()
+ val records = generateRunningRecords(5)
+ fake.insertRecords(records)
+
+ // Try to delete a record that doesn't exist
+ fake.deleteRecords(
+ records.first()::class,
+ recordIdsList = listOf(runRecord1.metadata.clientRecordId!!),
+ emptyList()
+ )
+ // Does not throw
+ }
+
+ @Test
+ fun pagination_singlePage() = runTest {
+ val fake = FakeHealthConnectClient()
+ val numberOfRecords = 3
+ val records = generateRunningRecords(numberOfRecords)
+
+ fake.insertRecords(records)
+
+ val pagedRequest1 =
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = records.first().startTime.minusSeconds(1),
+ endTime = records.last().endTime.plusSeconds(1)
+ ),
+ recordType = records.first()::class,
+ pageSize = 5,
+ )
+ val page1 = fake.readRecords(pagedRequest1)
+
+ assertThat(page1.records).hasSize(numberOfRecords)
+ assertThat(page1.pageToken).isNull()
+ }
+
+ @Test
+ fun pagination_threePages() = runTest {
+ val fake = FakeHealthConnectClient()
+ val records = generateRunningRecords(5)
+
+ fake.insertRecords(records)
+
+ val pagedRequest1 =
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = records.first().startTime.minusSeconds(1),
+ endTime = records.last().endTime.plusSeconds(1)
+ ),
+ recordType = records.first()::class,
+ pageSize = 2,
+ )
+ val page1 = fake.readRecords(pagedRequest1)
+
+ val pagedRequest2 =
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = records.first().startTime.minusSeconds(1),
+ endTime = records.last().endTime.plusSeconds(1)
+ ),
+ recordType = records.first()::class,
+ pageToken = page1.pageToken,
+ pageSize = 2,
+ )
+ val page2 = fake.readRecords(pagedRequest2)
+ val pagedRequest3 =
+ ReadRecordsRequest(
+ timeRangeFilter =
+ TimeRangeFilter(
+ startTime = records.first().startTime.minusSeconds(1),
+ endTime = records.last().endTime.plusSeconds(1)
+ ),
+ recordType = records.first()::class,
+ pageToken = page2.pageToken,
+ pageSize = 2,
+ )
+ val page3 = fake.readRecords(pagedRequest3)
+
+ assertThat(page1.records).hasSize(2)
+ assertThat(page2.records).hasSize(2)
+ assertThat(page3.records).hasSize(1)
+ assertThat(page3.pageToken).isNull()
+ assertThat(page2.records.first().title).isEqualTo(records[2].title)
+ assertThat(page3.records.first().title).isEqualTo(records[4].title)
+ }
+}
diff --git a/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakePermissionControllerTest.kt b/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakePermissionControllerTest.kt
new file mode 100644
index 0000000..5daae33
--- /dev/null
+++ b/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/FakePermissionControllerTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.testing
+
+import androidx.health.connect.client.permission.HealthPermission
+import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
+import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_WRITE_EXERCISE_ROUTE
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.runTest
+
+/** Unit tests for [FakePermissionController]. */
+class FakePermissionControllerTest {
+
+ @Test
+ fun grantAll_grantsWriteAndRead() = runTest {
+ val controller = FakePermissionController(grantAll = true)
+ val permissions = controller.getGrantedPermissions()
+ assertThat(permissions.size).isAtLeast(HealthPermission.ALL_PERMISSIONS.toSet().size)
+ }
+
+ @Test
+ fun grantAll_grantsExtraPermissions() = runTest {
+ val controller = FakePermissionController(grantAll = true)
+ val permissions = controller.getGrantedPermissions()
+ assertThat(permissions).contains(PERMISSION_WRITE_EXERCISE_ROUTE)
+ assertThat(permissions).contains(PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND)
+ }
+
+ @Test
+ fun grantAllFalse_noPermissions() = runTest {
+ val controller = FakePermissionController(grantAll = false)
+ val permissions = controller.getGrantedPermissions()
+ assertThat(permissions).hasSize(0)
+ }
+
+ @Test
+ fun grantPermission_grantsPermission() = runTest {
+ val controller = FakePermissionController(grantAll = false)
+ controller.grantPermission(PERMISSION_WRITE_EXERCISE_ROUTE)
+ val permissions = controller.getGrantedPermissions()
+ assertThat(permissions).contains(PERMISSION_WRITE_EXERCISE_ROUTE)
+ }
+
+ @Test
+ fun revokeAllPermissions_noPermissions() = runTest {
+ val controller = FakePermissionController(grantAll = true)
+ controller.revokeAllPermissions()
+ val permissions = controller.getGrantedPermissions()
+ assertThat(permissions).hasSize(0)
+ }
+
+ @Test
+ fun grantPermissions_doesntReplace() = runTest {
+ val controller = FakePermissionController(grantAll = false)
+ controller.grantPermission("permission1")
+ controller.grantPermissions(setOf("permission2"))
+ assertThat(controller.getGrantedPermissions()).hasSize(2)
+ }
+
+ @Test
+ fun revokePermissions() = runTest {
+ val controller = FakePermissionController(grantAll = false)
+ controller.grantPermission("permission1")
+ controller.grantPermission("permission2")
+ controller.revokePermission("permission1")
+ assertThat(controller.getGrantedPermissions()).hasSize(1)
+ }
+
+ @Test
+ fun replaceGrantedPermissions() = runTest {
+ val controller = FakePermissionController(grantAll = false)
+ controller.grantPermission("permission1")
+ controller.grantPermission("permission2")
+ controller.replaceGrantedPermissions(setOf("permission1", "permission3"))
+ assertThat(controller.getGrantedPermissions())
+ .isEqualTo(setOf("permission1", "permission3"))
+ }
+}
diff --git a/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/testdata/TestData.kt b/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/testdata/TestData.kt
new file mode 100644
index 0000000..2446c31
--- /dev/null
+++ b/health/connect/connect-testing/src/test/java/androidx/health/connect/client/testing/testdata/TestData.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.testing.testdata
+
+import androidx.health.connect.client.records.ExerciseSessionRecord
+import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.testing.FakeHealthConnectClient
+import androidx.health.connect.client.units.Volume
+import java.time.ZonedDateTime
+
+// Arbitrary start time in the past
+private val startTime: ZonedDateTime = ZonedDateTime.now().minusHours(23)
+
+val runRecord1 =
+ ExerciseSessionRecord(
+ startTime = startTime.plusMinutes(1).toInstant(),
+ startZoneOffset = startTime.offset,
+ endTime = startTime.plusMinutes(2).toInstant(),
+ endZoneOffset = startTime.offset,
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
+ title = "My Run #1",
+ exerciseRoute = null,
+ metadata =
+ Metadata(
+ clientRecordId = "FakeHealthConnectData1",
+ id = "Id1",
+ dataOrigin = DataOrigin(FakeHealthConnectClient.DEFAULT_PACKAGE_NAME)
+ )
+ )
+
+val hydrationRecord1 =
+ HydrationRecord(
+ startTime = startTime.plusMinutes(3).toInstant(),
+ startZoneOffset = startTime.offset,
+ endTime = startTime.plusMinutes(4).toInstant(),
+ endZoneOffset = startTime.offset,
+ volume = Volume.liters(1.0)
+ )
+
+/** Same as [runRecord1] but updated with a new end time. */
+val runRecord1Updated =
+ ExerciseSessionRecord(
+ startTime = startTime.plusMinutes(1).toInstant(),
+ startZoneOffset = startTime.offset,
+ endTime = startTime.plusMinutes(3).toInstant(),
+ endZoneOffset = startTime.offset,
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
+ title = "My Run #1 - Updated",
+ exerciseRoute = null,
+ metadata =
+ Metadata(
+ clientRecordId = "FakeHealthConnectData1",
+ id = "Id1",
+ dataOrigin = DataOrigin(FakeHealthConnectClient.DEFAULT_PACKAGE_NAME)
+ )
+ )
+
+/**
+ * Generates a list of [ExerciseSessionRecord]s of type
+ * [ExerciseSessionRecord.EXERCISE_TYPE_RUNNING], one per day ending now, 1h long.
+ */
+@JvmOverloads
+fun generateRunningRecords(
+ amount: Int,
+ startTime: ZonedDateTime = ZonedDateTime.now().minusDays(amount.toLong()),
+ exerciseType: Int = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
+ defaultPackageName: String = FakeHealthConnectClient.DEFAULT_PACKAGE_NAME
+): List<ExerciseSessionRecord> {
+ return List(amount) { index ->
+ val day = startTime.plusDays(index.toLong())
+ ExerciseSessionRecord(
+ startTime = day.minusMinutes(60).toInstant(),
+ startZoneOffset = startTime.offset,
+ endTime = day.toInstant(),
+ endZoneOffset = startTime.offset,
+ exerciseType = exerciseType,
+ title = "My Run #$index",
+ exerciseRoute = null,
+ metadata =
+ Metadata(
+ clientRecordId = "FakeHealthConnectDataRunning$index",
+ dataOrigin = DataOrigin(defaultPackageName)
+ )
+ )
+ }
+}
diff --git a/ink/ink-brush/build.gradle b/ink/ink-brush/build.gradle
index bb1e3f7..8083aae 100644
--- a/ink/ink-brush/build.gradle
+++ b/ink/ink-brush/build.gradle
@@ -23,15 +23,55 @@
*/
import androidx.build.LibraryType
+import androidx.build.PlatformIdentifier
plugins {
id("AndroidXPlugin")
- id("kotlin")
+ id("com.android.library")
}
-dependencies {
- api(project(":compose:ui:ui-graphics"))
- testImplementation(libs.kotlinTest)
+androidXMultiplatform {
+ android()
+ jvm()
+
+ defaultPlatform(PlatformIdentifier.JVM)
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ api(project(":compose:ui:ui-graphics"))
+ }
+ }
+
+ commonTest {
+ dependencies {
+ implementation(libs.kotlinTest)
+ }
+ }
+
+ androidMain {
+ dependsOn(commonMain)
+ }
+
+ androidInstrumentedTest {
+ dependsOn(commonTest)
+ dependencies {
+ implementation(libs.testRunner)
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ }
+ }
+}
+
+android {
+ namespace = "androidx.ink.brush"
}
androidx {
diff --git a/ink/ink-brush/src/main/java/androidx/ink/brush/Brush.kt b/ink/ink-brush/src/commonMain/kotlin/androidx/ink/brush/Brush.kt
similarity index 100%
rename from ink/ink-brush/src/main/java/androidx/ink/brush/Brush.kt
rename to ink/ink-brush/src/commonMain/kotlin/androidx/ink/brush/Brush.kt
diff --git a/ink/ink-brush/src/test/java/androidx/ink/brush/BrushTest.kt b/ink/ink-brush/src/commonTest/kotlin/androidx/ink/brush/BrushTest.kt
similarity index 100%
rename from ink/ink-brush/src/test/java/androidx/ink/brush/BrushTest.kt
rename to ink/ink-brush/src/commonTest/kotlin/androidx/ink/brush/BrushTest.kt
diff --git a/libraryversions.toml b/libraryversions.toml
index 541e5d5..a23cb69 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -12,11 +12,11 @@
BLUETOOTH = "1.0.0-alpha02"
BROWSER = "1.9.0-alpha01"
BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.4.0-beta02"
+CAMERA = "1.4.0-beta03"
CAMERA_PIPE = "1.0.0-alpha01"
CAMERA_TESTING = "1.0.0-alpha01"
-CAMERA_VIEWFINDER = "1.4.0-alpha07"
-CAMERA_VIEWFINDER_COMPOSE = "1.0.0-alpha02"
+CAMERA_VIEWFINDER = "1.4.0-alpha08"
+CAMERA_VIEWFINDER_COMPOSE = "1.0.0-alpha03"
CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.7.0-beta01"
COLLECTION = "1.5.0-alpha01"
@@ -57,7 +57,7 @@
DYNAMICANIMATION = "1.1.0-alpha04"
DYNAMICANIMATION_KTX = "1.0.0-alpha04"
EMOJI = "1.2.0-alpha03"
-EMOJI2 = "1.5.0-alpha01"
+EMOJI2 = "1.5.0-beta01"
ENTERPRISE = "1.1.0-rc01"
EXIFINTERFACE = "1.4.0-alpha01"
FRAGMENT = "1.8.0-rc01"
diff --git a/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt b/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
index 92cceb4..4dba651 100644
--- a/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
+++ b/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
@@ -28,6 +28,7 @@
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.UastLintUtils
import com.android.tools.lint.detector.api.isKotlin
+import com.intellij.psi.PsiAnnotationOwner
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiTypeParameter
import com.intellij.psi.PsiVariable
@@ -53,8 +54,8 @@
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.getUastParentOfType
import org.jetbrains.uast.isNullLiteral
-import org.jetbrains.uast.resolveToUElement
import org.jetbrains.uast.toUElement
+import org.jetbrains.uast.toUElementOfType
/**
* Lint check for ensuring that [androidx.lifecycle.MutableLiveData] values are never null when the
@@ -320,7 +321,12 @@
val isSuspendMethod = !context.evaluator.isSuspend(psiMethod)
return psiMethod.hasAnnotation(NULLABLE_ANNOTATION) && isSuspendMethod
} else if (this is UReferenceExpression) {
- return (resolveToUElement() as? UAnnotated)?.findAnnotation(NULLABLE_ANNOTATION) != null
+ val resolved = resolve()
+ return if (resolved is PsiAnnotationOwner) {
+ resolved.findAnnotation(NULLABLE_ANNOTATION) != null
+ } else {
+ resolved.toUElementOfType<UAnnotated>()?.findAnnotation(NULLABLE_ANNOTATION) != null
+ }
}
return false
}
diff --git a/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt b/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt
index 7e4c4e0..95ee2ed 100644
--- a/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt
+++ b/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt
@@ -1031,6 +1031,54 @@
.expectClean()
}
+ @Test
+ fun lambdaParameterFromMediatorLiveData() {
+ // Regression test from b/341316048
+ // https://youtrack.jetbrains.com/issue/KTIJ-30464
+ check(
+ kotlin(
+ """
+ package androidx.lifecycle
+
+ fun interface Observer<T> {
+ fun onChanged(value: T)
+ }
+ """
+ )
+ .indented(),
+ java(
+ """
+ package androidx.lifecycle;
+
+ public class MediatorLiveData<T> extends MutableLiveData<T> {
+ public <S> void addSource(LiveData<S> source, Observer<? super S> onChanged) {
+ }
+ }
+ """
+ )
+ .indented(),
+ kotlin(
+ """
+ import androidx.lifecycle.MediatorLiveData
+
+ class Test {
+ val myData = MediatorLiveData<List<Boolean>>()
+
+ init {
+ myData.addSource(getSources()) { data ->
+ myData.value = data
+ }
+ }
+
+ private fun getSources(): MediatorLiveData<List<Boolean>> = TODO()
+ }
+ """
+ )
+ .indented()
+ )
+ .expectClean()
+ }
+
private companion object {
val DATA_LIB: TestFile =
bytecode(
diff --git a/lint/lint-gradle/build.gradle b/lint/lint-gradle/build.gradle
index 0793c790..e578727 100644
--- a/lint/lint-gradle/build.gradle
+++ b/lint/lint-gradle/build.gradle
@@ -34,7 +34,6 @@
compileOnly(libs.androidLintChecks)
compileOnly(libs.kotlinStdlib)
- testImplementation(libs.kotlinStdlib)
testImplementation(libs.androidLint)
testImplementation(libs.androidLintTests)
testImplementation(libs.junit)
diff --git a/privacysandbox/ui/integration-tests/mediateesdkprovider/src/main/java/androidx/privacysandbox/ui/integration/mediateesdkprovider/MediateeSdkApi.kt b/privacysandbox/ui/integration-tests/mediateesdkprovider/src/main/java/androidx/privacysandbox/ui/integration/mediateesdkprovider/MediateeSdkApi.kt
index 392039f..96e38f2 100644
--- a/privacysandbox/ui/integration-tests/mediateesdkprovider/src/main/java/androidx/privacysandbox/ui/integration/mediateesdkprovider/MediateeSdkApi.kt
+++ b/privacysandbox/ui/integration-tests/mediateesdkprovider/src/main/java/androidx/privacysandbox/ui/integration/mediateesdkprovider/MediateeSdkApi.kt
@@ -18,31 +18,43 @@
import android.content.Context
import android.os.Bundle
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.AdType
import androidx.privacysandbox.ui.integration.sdkproviderutils.TestAdapters
+import androidx.privacysandbox.ui.integration.sdkproviderutils.ViewabilityHandler
import androidx.privacysandbox.ui.integration.testaidl.IMediateeSdkApi
import androidx.privacysandbox.ui.provider.toCoreLibInfo
class MediateeSdkApi(private val sdkContext: Context) : IMediateeSdkApi.Stub() {
private val testAdapters = TestAdapters(sdkContext)
- override fun loadBannerAd(@AdType adType: Int, withSlowDraw: Boolean): Bundle {
- return when (adType) {
- AdType.WEBVIEW -> loadWebViewBannerAd()
- AdType.WEBVIEW_FROM_LOCAL_ASSETS -> loadWebViewBannerAdFromLocalAssets()
- else -> loadNonWebViewBannerAd("Mediation", withSlowDraw)
- }
+ override fun loadBannerAd(
+ @AdType adType: Int,
+ withSlowDraw: Boolean,
+ drawViewability: Boolean
+ ): Bundle {
+ val adapter: SandboxedUiAdapter =
+ when (adType) {
+ AdType.WEBVIEW -> loadWebViewBannerAd()
+ AdType.WEBVIEW_FROM_LOCAL_ASSETS -> loadWebViewBannerAdFromLocalAssets()
+ else -> loadNonWebViewBannerAd("Mediation", withSlowDraw)
+ }
+ ViewabilityHandler.addObserverFactoryToAdapter(adapter, drawViewability)
+ return adapter.toCoreLibInfo(sdkContext)
}
- private fun loadWebViewBannerAd(): Bundle {
- return testAdapters.WebViewBannerAd().toCoreLibInfo(sdkContext)
+ private fun loadWebViewBannerAd(): SandboxedUiAdapter {
+ return testAdapters.WebViewBannerAd()
}
- private fun loadWebViewBannerAdFromLocalAssets(): Bundle {
- return testAdapters.WebViewAdFromLocalAssets().toCoreLibInfo(sdkContext)
+ private fun loadWebViewBannerAdFromLocalAssets(): SandboxedUiAdapter {
+ return testAdapters.WebViewAdFromLocalAssets()
}
- private fun loadNonWebViewBannerAd(text: String, waitInsideOnDraw: Boolean): Bundle {
- return testAdapters.TestBannerAd(text, waitInsideOnDraw).toCoreLibInfo(sdkContext)
+ private fun loadNonWebViewBannerAd(
+ text: String,
+ waitInsideOnDraw: Boolean
+ ): SandboxedUiAdapter {
+ return testAdapters.TestBannerAd(text, waitInsideOnDraw)
}
}
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/ViewabilityHandler.kt b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/ViewabilityHandler.kt
new file mode 100644
index 0000000..b03960d
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/ViewabilityHandler.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.integration.sdkproviderutils
+
+import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.drawable.GradientDrawable
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserver
+import androidx.privacysandbox.ui.core.SessionObserverContext
+import androidx.privacysandbox.ui.core.SessionObserverFactory
+
+class ViewabilityHandler {
+ companion object {
+
+ private const val TAG = "ViewabilityHandler"
+
+ fun addObserverFactoryToAdapter(adapter: SandboxedUiAdapter, drawViewability: Boolean) {
+ adapter.addObserverFactory(SessionObserverFactoryImpl(drawViewability))
+ }
+
+ private class SessionObserverFactoryImpl(val drawViewability: Boolean) :
+ SessionObserverFactory {
+
+ override fun create(): SessionObserver {
+ return SessionObserverImpl()
+ }
+
+ private inner class SessionObserverImpl : SessionObserver {
+ lateinit var view: View
+
+ override fun onSessionOpened(sessionObserverContext: SessionObserverContext) {
+ Log.i(TAG, "onSessionOpened $sessionObserverContext")
+ view = checkNotNull(sessionObserverContext.view)
+ }
+
+ override fun onUiContainerChanged(uiContainerInfo: Bundle) {
+ val sandboxedSdkViewUiInfo = SandboxedSdkViewUiInfo.fromBundle(uiContainerInfo)
+ if (drawViewability) {
+ // draw a red rectangle over the received onScreenGeometry of the view
+ drawRedRectangle(sandboxedSdkViewUiInfo.onScreenGeometry)
+ }
+ Log.i(TAG, "onUiContainerChanged $sandboxedSdkViewUiInfo")
+ }
+
+ override fun onSessionClosed() {
+ Log.i(TAG, "session closed")
+ }
+
+ private fun drawRedRectangle(bounds: Rect) {
+ view.overlay.clear()
+ val viewabilityRect =
+ GradientDrawable().apply {
+ shape = GradientDrawable.RECTANGLE
+ setStroke(10, Color.RED)
+ }
+ viewabilityRect.bounds = bounds
+ view.overlay.add(viewabilityRect)
+ }
+ }
+ }
+ }
+}
diff --git a/privacysandbox/ui/integration-tests/testaidl/src/main/aidl/androidx/privacysandbox/ui/integration/testaidl/IAppOwnedMediateeSdkApi.aidl b/privacysandbox/ui/integration-tests/testaidl/src/main/aidl/androidx/privacysandbox/ui/integration/testaidl/IAppOwnedMediateeSdkApi.aidl
index 0ec900e5..4174096 100644
--- a/privacysandbox/ui/integration-tests/testaidl/src/main/aidl/androidx/privacysandbox/ui/integration/testaidl/IAppOwnedMediateeSdkApi.aidl
+++ b/privacysandbox/ui/integration-tests/testaidl/src/main/aidl/androidx/privacysandbox/ui/integration/testaidl/IAppOwnedMediateeSdkApi.aidl
@@ -19,5 +19,5 @@
import android.os.Bundle;
interface IAppOwnedMediateeSdkApi {
- Bundle loadBannerAd(int adType, boolean withSlowDraw);
+ Bundle loadBannerAd(int adType, boolean withSlowDraw, boolean drawViewability);
}
diff --git a/privacysandbox/ui/integration-tests/testaidl/src/main/aidl/androidx/privacysandbox/ui/integration/testaidl/IMediateeSdkApi.aidl b/privacysandbox/ui/integration-tests/testaidl/src/main/aidl/androidx/privacysandbox/ui/integration/testaidl/IMediateeSdkApi.aidl
index e91b821..68995c1 100644
--- a/privacysandbox/ui/integration-tests/testaidl/src/main/aidl/androidx/privacysandbox/ui/integration/testaidl/IMediateeSdkApi.aidl
+++ b/privacysandbox/ui/integration-tests/testaidl/src/main/aidl/androidx/privacysandbox/ui/integration/testaidl/IMediateeSdkApi.aidl
@@ -19,5 +19,5 @@
import android.os.Bundle;
interface IMediateeSdkApi {
- Bundle loadBannerAd(int adType, boolean withSlowDraw);
+ Bundle loadBannerAd(int adType, boolean withSlowDraw, boolean drawViewability);
}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/AppOwnedMediateeSdkApi.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/AppOwnedMediateeSdkApi.kt
index f663049..0abd0aa 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/AppOwnedMediateeSdkApi.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/AppOwnedMediateeSdkApi.kt
@@ -18,31 +18,43 @@
import android.content.Context
import android.os.Bundle
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.AdType
import androidx.privacysandbox.ui.integration.sdkproviderutils.TestAdapters
+import androidx.privacysandbox.ui.integration.sdkproviderutils.ViewabilityHandler
import androidx.privacysandbox.ui.integration.testaidl.IAppOwnedMediateeSdkApi
import androidx.privacysandbox.ui.provider.toCoreLibInfo
-class AppOwnedMediateeSdkApi(val sdkContext: Context) : IAppOwnedMediateeSdkApi.Stub() {
+class AppOwnedMediateeSdkApi(private val sdkContext: Context) : IAppOwnedMediateeSdkApi.Stub() {
private val testAdapters = TestAdapters(sdkContext)
- override fun loadBannerAd(@AdType adType: Int, waitInsideOnDraw: Boolean): Bundle {
- return when (adType) {
- AdType.WEBVIEW -> loadWebViewBannerAd()
- AdType.WEBVIEW_FROM_LOCAL_ASSETS -> loadWebViewBannerAdFromLocalAssets()
- else -> loadNonWebViewBannerAd("AppOwnedMediation", waitInsideOnDraw)
- }
+ override fun loadBannerAd(
+ @AdType adType: Int,
+ waitInsideOnDraw: Boolean,
+ drawViewability: Boolean
+ ): Bundle {
+ val adapter: SandboxedUiAdapter =
+ when (adType) {
+ AdType.WEBVIEW -> loadWebViewBannerAd()
+ AdType.WEBVIEW_FROM_LOCAL_ASSETS -> loadWebViewBannerAdFromLocalAssets()
+ else -> loadNonWebViewBannerAd("AppOwnedMediation", waitInsideOnDraw)
+ }
+ ViewabilityHandler.addObserverFactoryToAdapter(adapter, drawViewability)
+ return adapter.toCoreLibInfo(sdkContext)
}
- private fun loadWebViewBannerAd(): Bundle {
- return testAdapters.WebViewBannerAd().toCoreLibInfo(sdkContext)
+ private fun loadWebViewBannerAd(): SandboxedUiAdapter {
+ return testAdapters.WebViewBannerAd()
}
- private fun loadWebViewBannerAdFromLocalAssets(): Bundle {
- return testAdapters.WebViewAdFromLocalAssets().toCoreLibInfo(sdkContext)
+ private fun loadWebViewBannerAdFromLocalAssets(): SandboxedUiAdapter {
+ return testAdapters.WebViewAdFromLocalAssets()
}
- private fun loadNonWebViewBannerAd(text: String, waitInsideOnDraw: Boolean): Bundle {
- return testAdapters.TestBannerAd(text, waitInsideOnDraw).toCoreLibInfo(sdkContext)
+ private fun loadNonWebViewBannerAd(
+ text: String,
+ waitInsideOnDraw: Boolean
+ ): SandboxedUiAdapter {
+ return testAdapters.TestBannerAd(text, waitInsideOnDraw)
}
}
diff --git a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
index 5eaac6d..5944650 100644
--- a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
+++ b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
@@ -17,24 +17,17 @@
package androidx.privacysandbox.ui.integration.testsdkprovider
import android.content.Context
-import android.graphics.Color
-import android.graphics.Rect
-import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.os.Process
-import android.util.Log
import android.view.View
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
import androidx.privacysandbox.ui.client.view.SandboxedSdkView
-import androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo
import androidx.privacysandbox.ui.core.SandboxedUiAdapter
-import androidx.privacysandbox.ui.core.SessionObserver
-import androidx.privacysandbox.ui.core.SessionObserverContext
-import androidx.privacysandbox.ui.core.SessionObserverFactory
import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.AdType
import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.MediationOption
import androidx.privacysandbox.ui.integration.sdkproviderutils.TestAdapters
+import androidx.privacysandbox.ui.integration.sdkproviderutils.ViewabilityHandler
import androidx.privacysandbox.ui.integration.testaidl.IAppOwnedMediateeSdkApi
import androidx.privacysandbox.ui.integration.testaidl.IMediateeSdkApi
import androidx.privacysandbox.ui.integration.testaidl.ISdkApi
@@ -42,7 +35,6 @@
class SdkApi(private val sdkContext: Context) : ISdkApi.Stub() {
private val testAdapters = TestAdapters(sdkContext)
- private val measurementManager = MeasurementManager()
override fun loadBannerAd(
@AdType adType: Int,
@@ -56,7 +48,7 @@
val isAppOwnedMediation = (mediationOption == MediationOption.IN_APP_MEDIATEE)
val adapter: SandboxedUiAdapter =
if (isMediation) {
- loadMediatedTestAd(isAppOwnedMediation, adType, waitInsideOnDraw)
+ loadMediatedTestAd(isAppOwnedMediation, adType, waitInsideOnDraw, drawViewability)
} else {
when (adType) {
AdType.NON_WEBVIEW -> {
@@ -71,9 +63,8 @@
else -> {
loadNonWebViewBannerAd("Ad type not present", waitInsideOnDraw)
}
- }
+ }.also { ViewabilityHandler.addObserverFactoryToAdapter(it, drawViewability) }
}
- measurementManager.startObserving(adapter, drawViewability)
return adapter.toCoreLibInfo(sdkContext)
}
@@ -100,10 +91,11 @@
private fun loadMediatedTestAd(
isAppMediatee: Boolean,
@AdType adType: Int,
- waitInsideOnDraw: Boolean = false
+ waitInsideOnDraw: Boolean,
+ drawViewability: Boolean
): SandboxedUiAdapter {
val mediateeBannerAdBundle =
- getMediateeBannerAdBundle(isAppMediatee, adType, waitInsideOnDraw)
+ getMediateeBannerAdBundle(isAppMediatee, adType, waitInsideOnDraw, drawViewability)
return MediatedBannerAd(mediateeBannerAdBundle)
}
@@ -128,7 +120,8 @@
private fun getMediateeBannerAdBundle(
isAppMediatee: Boolean,
adType: Int,
- withSlowDraw: Boolean
+ withSlowDraw: Boolean,
+ drawViewability: Boolean
): Bundle? {
val sdkSandboxControllerCompat = SdkSandboxControllerCompat.from(sdkContext)
if (isAppMediatee) {
@@ -140,7 +133,11 @@
IAppOwnedMediateeSdkApi.Stub.asInterface(
appOwnedSdkSandboxInterfaceCompat.getInterface()
)
- return appOwnedMediateeSdkApi.loadBannerAd(adType, withSlowDraw)
+ return appOwnedMediateeSdkApi.loadBannerAd(
+ adType,
+ withSlowDraw,
+ drawViewability
+ )
}
}
} else {
@@ -149,60 +146,13 @@
if (sandboxedSdkCompat.getSdkInfo()?.name == MEDIATEE_SDK) {
val mediateeSdkApi =
IMediateeSdkApi.Stub.asInterface(sandboxedSdkCompat.getInterface())
- return mediateeSdkApi.loadBannerAd(adType, withSlowDraw)
+ return mediateeSdkApi.loadBannerAd(adType, withSlowDraw, drawViewability)
}
}
}
return null
}
- class MeasurementManager {
- fun startObserving(adapter: SandboxedUiAdapter, drawViewability: Boolean) {
- adapter.addObserverFactory(SessionObserverFactoryImpl(drawViewability))
- }
-
- private inner class SessionObserverFactoryImpl(val drawViewability: Boolean) :
- SessionObserverFactory {
-
- override fun create(): SessionObserver {
- return SessionObserverImpl()
- }
-
- private inner class SessionObserverImpl : SessionObserver {
- lateinit var view: View
-
- override fun onSessionOpened(sessionObserverContext: SessionObserverContext) {
- Log.i(TAG, "onSessionOpened $sessionObserverContext")
- view = checkNotNull(sessionObserverContext.view)
- }
-
- override fun onUiContainerChanged(uiContainerInfo: Bundle) {
- val sandboxedSdkViewUiInfo = SandboxedSdkViewUiInfo.fromBundle(uiContainerInfo)
- if (drawViewability) {
- // draw a red rectangle over the received onScreenGeometry of the view
- drawRedRectangle(sandboxedSdkViewUiInfo.onScreenGeometry)
- }
- Log.i(TAG, "onUiContainerChanged $sandboxedSdkViewUiInfo")
- }
-
- override fun onSessionClosed() {
- Log.i(TAG, "session closed")
- }
-
- private fun drawRedRectangle(bounds: Rect) {
- view.overlay.clear()
- val viewabilityRect =
- GradientDrawable().apply {
- shape = GradientDrawable.RECTANGLE
- setStroke(10, Color.RED)
- }
- viewabilityRect.bounds = bounds
- view.overlay.add(viewabilityRect)
- }
- }
- }
- }
-
companion object {
private const val MEDIATEE_SDK =
"androidx.privacysandbox.ui.integration.mediateesdkprovider"
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SyncTriggersConcurrencyTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SyncTriggersConcurrencyTest.kt
index 337a025..952c29b 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SyncTriggersConcurrencyTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SyncTriggersConcurrencyTest.kt
@@ -21,7 +21,6 @@
import androidx.room.Database
import androidx.room.Delete
import androidx.room.Entity
-import androidx.room.Ignore
import androidx.room.Insert
import androidx.room.InvalidationTracker
import androidx.room.OnConflictStrategy
@@ -38,6 +37,7 @@
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
+import kotlin.test.Ignore
import kotlin.test.assertTrue
import org.junit.After
import org.junit.Before
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
index 897e239..3b4cffab 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
@@ -40,7 +40,7 @@
) :
KspType(
env = env,
- ksType = typeArg.requireType(),
+ ksType = ksType,
originalKSAnnotations = originalKSAnnotations,
scope = scope,
typeAlias = typeAlias,
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
index f649b1e..ed9ea59 100644
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
@@ -23,6 +23,7 @@
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.SQLiteStatement
+import androidx.test.filters.FlakyTest
import java.lang.ref.WeakReference
import java.util.Locale
import java.util.concurrent.CountDownLatch
@@ -313,6 +314,7 @@
}
@Test
+ @FlakyTest(bugId = 349880963)
fun multipleRefreshAsync() = runTest {
// Validate that when multiple refresh are enqueued, that only one runs.
tracker.refreshAsync()
diff --git a/stableaidl/stableaidl-gradle-plugin/build.gradle b/stableaidl/stableaidl-gradle-plugin/build.gradle
index 13fded7..4518da8 100644
--- a/stableaidl/stableaidl-gradle-plugin/build.gradle
+++ b/stableaidl/stableaidl-gradle-plugin/build.gradle
@@ -40,15 +40,17 @@
}
dependencies {
- implementation(findGradleKotlinDsl())
+ runtimeOnly(findGradleKotlinDsl())
+
+ api(libs.androidGradlePluginApi)
+ api(libs.androidToolsCommon)
+ api(libs.androidToolsSdkCommon)
+ api(libs.guava)
+
implementation(gradleApi())
- implementation(libs.androidGradlePluginApi)
- implementation(libs.androidToolsCommon)
implementation(libs.androidToolsRepository)
- implementation(libs.androidToolsSdkCommon)
implementation(libs.apacheCommonIo)
implementation(libs.apacheAnt)
- implementation(libs.guava)
implementation(libs.kotlinStdlib)
testPlugin(libs.androidGradlePluginz)
@@ -57,9 +59,11 @@
testImplementation(gradleTestKit())
testImplementation(project(":internal-testutils-gradle-plugin"))
testImplementation(libs.androidToolsAnalyticsProtos)
- testImplementation(libs.gson)
+ testImplementation(libs.asm)
+ testImplementation(libs.builder)
testImplementation(libs.junit)
testImplementation(libs.kotlinTest)
+ testImplementation(libs.gson)
testImplementation(libs.truth)
}
diff --git a/test/uiautomator/integration-tests/testapp/build.gradle b/test/uiautomator/integration-tests/testapp/build.gradle
index 0efa882..e84d3fc 100644
--- a/test/uiautomator/integration-tests/testapp/build.gradle
+++ b/test/uiautomator/integration-tests/testapp/build.gradle
@@ -23,16 +23,19 @@
dependencies {
implementation(libs.kotlinStdlib)
-
- implementation("androidx.annotation:annotation:1.4.0")
+ implementation(libs.androidx.annotation)
implementation("androidx.core:core:1.6.0")
+ implementation project(":activity:activity")
implementation(project(":activity:activity-compose"))
+ implementation project(":compose:foundation:foundation")
+ implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":compose:material:material"))
- implementation(project(":compose:animation:animation"))
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui"))
- implementation(project(":compose:foundation:foundation-layout"))
+ implementation project(":compose:ui:ui-graphics")
+ implementation project(":compose:ui:ui-text")
+ implementation project(":compose:ui:ui-unit")
androidTestImplementation(project(":test:uiautomator:uiautomator"))
androidTestImplementation(libs.testCore)
@@ -40,8 +43,10 @@
androidTestImplementation(libs.testRunner)
// Align dependencies in debugRuntimeClasspath and debugAndroidTestRuntimeClasspath.
- androidTestImplementation("androidx.lifecycle:lifecycle-common:2.8.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-common:2.8.3")
androidTestImplementation("androidx.annotation:annotation:1.8.0")
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.testMonitor)
}
android {
diff --git a/test/uiautomator/uiautomator/build.gradle b/test/uiautomator/uiautomator/build.gradle
index 0079b25..a25a404 100644
--- a/test/uiautomator/uiautomator/build.gradle
+++ b/test/uiautomator/uiautomator/build.gradle
@@ -33,10 +33,13 @@
implementation("androidx.annotation:annotation:1.4.0")
implementation("androidx.tracing:tracing:1.1.0")
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testMonitor)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.dexmakerMockitoInline)
+
+ androidTestRuntimeOnly(libs.dexmakerMockitoInline)
}
android {
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/CircularProgressIndicatorSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/CircularProgressIndicatorSample.kt
index 50a8d62..e164114 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/CircularProgressIndicatorSample.kt
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/CircularProgressIndicatorSample.kt
@@ -24,7 +24,6 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.progressSemantics
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -41,10 +40,7 @@
@Sampled
@Composable
public fun IndeterminateCircularProgressIndicator() {
- CircularProgressIndicator(
- // Set progress semantics for accessibility.
- modifier = Modifier.progressSemantics()
- )
+ CircularProgressIndicator()
}
@Sampled
@@ -59,8 +55,6 @@
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator(
- // Set progress semantics for accessibility.
- modifier = Modifier.progressSemantics(animatedProgress),
progress = animatedProgress,
)
Spacer(Modifier.requiredHeight(10.dp))
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
index 80a211a..cf1c04b 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
@@ -22,11 +22,6 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.ProgressBarRangeInfo
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.semantics.progressBarRangeInfo
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertRangeInfoEquals
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
@@ -50,27 +45,11 @@
}
@Test
- fun has_no_semantics_by_default() {
+ fun shows_indeterminate_progress() {
rule.setContentWithTheme {
CircularProgressIndicator(modifier = Modifier.testTag(TEST_TAG))
}
- rule
- .onNodeWithTag(TEST_TAG)
- .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.ProgressBarRangeInfo))
- }
-
- @Test
- fun allows_semantics_to_be_added_correctly() {
- rule.setContentWithTheme {
- CircularProgressIndicator(
- modifier =
- Modifier.testTag(TEST_TAG).semantics {
- progressBarRangeInfo = ProgressBarRangeInfo.Indeterminate
- },
- )
- }
-
rule.onNodeWithTag(TEST_TAG).assertRangeInfoEquals(ProgressBarRangeInfo.Indeterminate)
}
@@ -113,15 +92,12 @@
}
@Test
- fun allows_semantics_to_be_added_correctly() {
+ fun changes_progress() {
val progress = mutableStateOf(0f)
rule.setContentWithTheme {
CircularProgressIndicator(
- modifier =
- Modifier.testTag(TEST_TAG).semantics {
- progressBarRangeInfo = ProgressBarRangeInfo(progress.value, 0f..1f)
- },
+ modifier = Modifier.testTag(TEST_TAG),
progress = progress.value
)
}
@@ -199,6 +175,38 @@
.assertColorInPercentageRange(Color.Red, 5f..8f)
}
+ @Test
+ fun coerces_highest_out_of_bound_progress() {
+ val progress = mutableStateOf(0f)
+
+ rule.setContentWithTheme {
+ CircularProgressIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ progress = progress.value
+ )
+ }
+
+ rule.runOnIdle { progress.value = 1.5f }
+
+ rule.onNodeWithTag(TEST_TAG).assertRangeInfoEquals(ProgressBarRangeInfo(1f, 0f..1f))
+ }
+
+ @Test
+ fun coerces_lowest_out_of_bound_progress() {
+ val progress = mutableStateOf(0f)
+
+ rule.setContentWithTheme {
+ CircularProgressIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ progress = progress.value
+ )
+ }
+
+ rule.runOnIdle { progress.value = -1.5f }
+
+ rule.onNodeWithTag(TEST_TAG).assertRangeInfoEquals(ProgressBarRangeInfo(0f, 0f..1f))
+ }
+
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun set_small_stroke_width() {
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ProgressIndicator.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ProgressIndicator.kt
index ef15302..6d8bab1 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ProgressIndicator.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ProgressIndicator.kt
@@ -13,8 +13,10 @@
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.progressSemantics
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@@ -49,8 +51,6 @@
*
* Progress indicators express the proportion of completion of an ongoing task.
*
- * This is a purely visual component that is not focusable for accessibility purposes.
- *
* [Progress Indicator
* doc](https://developer.android.com/training/wearables/components/progress-indicator)  ![Progress
* indicator
@@ -197,26 +200,30 @@
// Canvas internally uses Spacer.drawBehind.
// Using Spacer.drawWithCache to optimize the stroke allocations.
Spacer(
- modifier.size(IndeterminateCircularIndicatorDiameter).drawWithCache {
- val stroke = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
+ modifier
+ .progressSemantics()
+ .size(IndeterminateCircularIndicatorDiameter)
+ .focusable()
+ .drawWithCache {
+ val stroke = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
- val currentRotationAngleOffset = (currentRotation * RotationAngleOffset) % 360f
- // How long a line to draw using the start angle as a reference point
- val sweep = abs(endAngle - startProgressAngle)
+ val currentRotationAngleOffset = (currentRotation * RotationAngleOffset) % 360f
+ // How long a line to draw using the start angle as a reference point
+ val sweep = abs(endAngle - startProgressAngle)
- // Offset by the constant offset and the per rotation offset
- val offset = (startAngle + currentRotationAngleOffset + baseRotation) % 360f
+ // Offset by the constant offset and the per rotation offset
+ val offset = (startAngle + currentRotationAngleOffset + baseRotation) % 360f
- onDrawWithContent {
- drawCircularIndicator(0f, 360f, trackColor, stroke)
- drawIndeterminateCircularIndicator(
- startProgressAngle + offset,
- sweep,
- indicatorColor,
- stroke
- )
+ onDrawWithContent {
+ drawCircularIndicator(0f, 360f, trackColor, stroke)
+ drawIndeterminateCircularIndicator(
+ startProgressAngle + offset,
+ sweep,
+ indicatorColor,
+ stroke
+ )
+ }
}
- }
)
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ProgressIndicator.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ProgressIndicator.kt
index 2efefa3..cdfab19 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ProgressIndicator.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ProgressIndicator.kt
@@ -16,6 +16,7 @@
package androidx.wear.compose.material3
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
@@ -30,6 +31,7 @@
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material3.ProgressIndicatorDefaults.StartAngle
@@ -44,8 +46,6 @@
/**
* Material Design circular progress indicator.
*
- * This is a purely visual component that is not focusable for accessibility purposes.
- *
* Example of a full screen [CircularProgressIndicator]. Note that the padding
* [ProgressIndicatorDefaults.FullScreenPadding] should be applied:
*
@@ -90,35 +90,40 @@
// Canvas internally uses Spacer.drawBehind.
// Using Spacer.drawWithCache to optimize the stroke allocations.
Spacer(
- modifier.fillMaxSize().drawWithCache {
- val fullSweep = 360f - ((startAngle - endAngle) % 360 + 360) % 360
- val progressSweep = fullSweep * coercedProgress()
- val stroke = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
- val minSize = min(size.height, size.width)
- // Sweep angle between two progress indicator segments.
- val gapSweep =
- asin((stroke.width + gapSize.toPx()) / (minSize - stroke.width)).toDegrees() * 2f
+ modifier
+ .clearAndSetSemantics {}
+ .fillMaxSize()
+ .focusable()
+ .drawWithCache {
+ val fullSweep = 360f - ((startAngle - endAngle) % 360 + 360) % 360
+ val progressSweep = fullSweep * coercedProgress()
+ val stroke = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
+ val minSize = min(size.height, size.width)
+ // Sweep angle between two progress indicator segments.
+ val gapSweep =
+ asin((stroke.width + gapSize.toPx()) / (minSize - stroke.width)).toDegrees() *
+ 2f
- onDrawWithContent {
- // Draw an indicator.
- drawIndicatorSegment(
- startAngle = startAngle,
- sweep = progressSweep,
- gapSweep = gapSweep,
- brush = colors.indicatorBrush,
- stroke = stroke
- )
+ onDrawWithContent {
+ // Draw an indicator.
+ drawIndicatorSegment(
+ startAngle = startAngle,
+ sweep = progressSweep,
+ gapSweep = gapSweep,
+ brush = colors.indicatorBrush,
+ stroke = stroke
+ )
- // Draw a background.
- drawIndicatorSegment(
- startAngle = startAngle + progressSweep,
- sweep = fullSweep - progressSweep,
- gapSweep = gapSweep,
- brush = colors.trackBrush,
- stroke = stroke
- )
+ // Draw a background.
+ drawIndicatorSegment(
+ startAngle = startAngle + progressSweep,
+ sweep = fullSweep - progressSweep,
+ gapSweep = gapSweep,
+ brush = colors.trackBrush,
+ stroke = stroke
+ )
+ }
}
- }
)
}
diff --git a/wear/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index 64f02c2..bda50a9 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -54,6 +54,6 @@
implementation(project(':wear:compose:compose-navigation'))
// Align dependencies in debugRuntimeClasspath and debugAndroidTestRuntimeClasspath.
- androidTestImplementation("androidx.lifecycle:lifecycle-common:2.8.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-common:2.8.3")
androidTestImplementation("androidx.annotation:annotation:1.8.0")
}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/ProtoLayoutTheme.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/ProtoLayoutTheme.java
index a1d75e4..0361d51 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/ProtoLayoutTheme.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/ProtoLayoutTheme.java
@@ -53,9 +53,8 @@
* supported, defaults to the system font.
*
* <p>It's theme's responsibility to define which font family is supported by returning the
- * corresponding {@link FontSet}. The default one from {@link
- * androidx.wear.protolayout.LayoutElementBuilders.FontStyle#DEFAULT_SYSTEM_FONT} should
- * be system font and always supported. The Roboto Flex variable font from {@link
+ * corresponding {@link FontSet}. The default one should be system font and always supported.
+ * The Roboto Flex variable font from {@link
* androidx.wear.protolayout.LayoutElementBuilders.FontStyle#ROBOTO_FLEX_FONT} and
* standard Roboto font from {@link
* androidx.wear.protolayout.LayoutElementBuilders.FontStyle#ROBOTO_FONT} should be
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutThemeImpl.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutThemeImpl.java
index 907abf1..92494b2 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutThemeImpl.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutThemeImpl.java
@@ -194,9 +194,8 @@
* supported, defaults to the system font.
*
* <p>It's theme's responsibility to define which font families are supported by returning the
- * corresponding {@link FontSet}. The default one from {@link
- * androidx.wear.protolayout.LayoutElementBuilders.FontStyle#DEFAULT_SYSTEM_FONT} should
- * be system font and always supported. The Roboto Flex variable font from {@link
+ * corresponding {@link FontSet}. The default one should be system font and always supported.
+ * The Roboto Flex variable font from {@link
* androidx.wear.protolayout.LayoutElementBuilders.FontStyle#ROBOTO_FLEX_FONT} and
* standard Roboto font from {@link
* androidx.wear.protolayout.LayoutElementBuilders.FontStyle#ROBOTO_FONT} should be
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index a8557cf..721be43 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -591,7 +591,6 @@
method public androidx.wear.protolayout.TypeBuilders.BoolProp? getUnderline();
method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.FontVariantProp? getVariant();
method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp? getWeight();
- field @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static final String DEFAULT_SYSTEM_FONT = "default";
field @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static final String ROBOTO_FLEX_FONT = "roboto-flex";
field @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static final String ROBOTO_FONT = "roboto";
}
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index a8557cf..721be43 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -591,7 +591,6 @@
method public androidx.wear.protolayout.TypeBuilders.BoolProp? getUnderline();
method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.FontVariantProp? getVariant();
method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp? getWeight();
- field @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static final String DEFAULT_SYSTEM_FONT = "default";
field @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static final String ROBOTO_FLEX_FONT = "roboto-flex";
field @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static final String ROBOTO_FONT = "roboto";
}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
index aba9251..f76e662 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
@@ -739,16 +739,10 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@StringDef(
- value = {DEFAULT_SYSTEM_FONT, ROBOTO_FONT, ROBOTO_FLEX_FONT},
+ value = {ROBOTO_FONT, ROBOTO_FLEX_FONT},
open = true)
public @interface FontFamilyName {}
- /**
- * Font family name that uses default system font. Supported in any renderer version.
- */
- @RequiresSchemaVersion(major = 1, minor = 400)
- public static final String DEFAULT_SYSTEM_FONT = "default";
-
/** Font family name that uses Roboto font. Supported in renderers supporting 1.4. */
@RequiresSchemaVersion(major = 1, minor = 400)
public static final String ROBOTO_FONT = "roboto";
@@ -1078,9 +1072,8 @@
* <p>If the given font family is not available on a device, the fallback values will be
* attempted to use, in order in which they are given.
*
- * <p>Renderer support for values outside of the given constants ( {@link
- * #DEFAULT_SYSTEM_FONT}, {@link #ROBOTO_FONT} or {@link #ROBOTO_FLEX_FONT}) is not
- * guaranteed for all devices.
+ * <p>Renderer support for values outside of the given constants ({@link #ROBOTO_FONT}
+ * or {@link #ROBOTO_FLEX_FONT}) is not guaranteed for all devices.
*
* <p>If not set, default system font will be used.
*
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java
index 8ad2a9c..6d01778 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java
@@ -16,15 +16,12 @@
package androidx.window.extensions.area;
-import android.annotation.SuppressLint;
import android.app.Activity;
-import android.os.Build;
import android.util.DisplayMetrics;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.window.extensions.RequiresVendorApiLevel;
import androidx.window.extensions.WindowExtensions;
@@ -162,20 +159,6 @@
@NonNull Consumer<@WindowAreaSessionState Integer> consumer);
/**
- * @deprecated Use {@link #startRearDisplaySession(Activity, Consumer)}.
- */
- @RequiresVendorApiLevel(level = 2)
- @Deprecated
- @SuppressLint("ClassVerificationFailure")
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @RequiresApi(api = Build.VERSION_CODES.N)
- default void startRearDisplaySession(@NonNull Activity activity,
- @NonNull java.util.function.Consumer<@WindowAreaSessionState Integer> consumer) {
- final Consumer<Integer> extensionsConsumer = consumer::accept;
- startRearDisplaySession(activity, extensionsConsumer);
- }
-
- /**
* Ends a RearDisplaySession and sends [STATE_INACTIVE] to the consumer
* provided in the {@code startRearDisplaySession} method. This method is only
* called through the {@code RearDisplaySession} provided to the developer.