Merge "Remove onSurfaceVariant2 from Compose for Wear OS Material Theme Color class" into androidx-main
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/CreateLibraryBuildInfoFileTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/CreateLibraryBuildInfoFileTask.kt
index 5b27d06..4ec1db8 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/CreateLibraryBuildInfoFileTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/CreateLibraryBuildInfoFileTask.kt
@@ -17,8 +17,8 @@
package androidx.build
import androidx.build.gitclient.Commit
-import androidx.build.gitclient.GitClient
import androidx.build.gitclient.GitCommitRange
+import androidx.build.gitclient.GitRunnerGitClient
import androidx.build.jetpad.LibraryBuildInfoFile
import com.google.gson.GsonBuilder
import org.gradle.api.DefaultTask
@@ -233,13 +233,8 @@
* of the build that is released. Thus, we use frameworks/support to get the sha
*/
private fun Project.getFrameworksSupportCommitShaAtHead(): String {
- val gitClient = GitClient.create(
- project.getSupportRootFolder(),
- logger,
- GitClient.getChangeInfoPath(project).get()
- )
val commitList: List<Commit> =
- gitClient
+ GitRunnerGitClient(project.getSupportRootFolder(), logger)
.getGitLog(
GitCommitRange(
fromExclusive = "",
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
index 31b60cb..ac41dbe 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
@@ -18,7 +18,7 @@
import androidx.build.dependencyTracker.AffectedModuleDetector.Companion.ENABLE_ARG
import androidx.build.getDistributionDirectory
-import androidx.build.gitclient.GitClient
+import androidx.build.gitclient.GitRunnerGitClient
import androidx.build.gradle.isRoot
import java.io.File
import org.gradle.api.Action
@@ -146,8 +146,6 @@
if (baseCommitOverride != null) {
logger.info("using base commit override $baseCommitOverride")
}
- val changeInfoPath = GitClient.getChangeInfoPath(rootProject)
- .forUseAtConfigurationTime()
gradle.taskGraph.whenReady {
logger.lifecycle("projects evaluated")
val projectGraph = ProjectGraph(rootProject)
@@ -161,7 +159,6 @@
params.dependencyTracker = dependencyTracker
params.log = outputFile
params.baseCommitOverride = baseCommitOverride
- params.changeInfoPath = changeInfoPath
}
)
logger.info("using real detector")
@@ -263,7 +260,6 @@
var alwaysBuildIfExists: Set<String>?
var ignoredPaths: Set<String>?
var baseCommitOverride: String?
- var changeInfoPath: Provider<String>
}
val detector: AffectedModuleDetector by lazy {
@@ -275,10 +271,9 @@
if (baseCommitOverride != null) {
logger.info("using base commit override $baseCommitOverride")
}
- val gitClient = GitClient.create(
- rootProjectDir = parameters.rootDir,
- logger = logger,
- changeInfoPath = parameters.changeInfoPath.get()
+ val gitClient = GitRunnerGitClient(
+ workingDir = parameters.rootDir,
+ logger = logger
)
val changedFilesProvider: ChangedFilesProvider = {
val baseSha = baseCommitOverride ?: gitClient.findPreviousSubmittedChange()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
index 9137ebf..a30fa85 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
@@ -19,9 +19,6 @@
import androidx.build.releasenotes.getBuganizerLink
import androidx.build.releasenotes.getChangeIdAOSPLink
import java.io.File
-import org.gradle.api.Project
-import org.gradle.api.provider.Provider
-import org.gradle.api.logging.Logger
interface GitClient {
fun findChangedFilesSince(
@@ -50,24 +47,6 @@
*/
fun executeAndParse(command: String): List<String>
}
-
- companion object {
- fun getChangeInfoPath(project: Project): Provider<String> {
- return project.providers.environmentVariable("CHANGE_INFO")
- }
- fun create(rootProjectDir: File, logger: Logger, changeInfoPath: String): GitClient {
- if (changeInfoPath != "") {
- val changeInfoFile = File(changeInfoPath)
- if (changeInfoFile.exists()) {
- val text = changeInfoFile.readText()
- logger.info("Using ChangeInfoGitClient, config = " + changeInfoPath)
- return ChangeInfoGitClient(text)
- }
- }
- logger.info("UsingGitRunnerGitClient")
- return GitRunnerGitClient(rootProjectDir, logger)
- }
- }
}
enum class CommitType {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
new file mode 100644
index 0000000..1a270eb
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.lazy.layout
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyLayoutTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun lazyListShowsCombinedItems() {
+ val counter = mutableStateOf(0)
+ var remeasureCount = 0
+ val policy = LazyMeasurePolicy { _, _ ->
+ remeasureCount++
+ object : LazyLayoutMeasureResult {
+ override val visibleItemsInfo: List<LazyLayoutItemInfo> = emptyList()
+ override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
+ override val height: Int = 10
+ override val width: Int = 10
+ override fun placeChildren() {}
+ }
+ }
+ val itemsProvider = {
+ object : LazyLayoutItemsProvider {
+ override fun getContent(index: Int): @Composable () -> Unit = {}
+ override val itemsCount: Int = 0
+ override fun getKey(index: Int) = Unit
+ override val keyToIndexMap: Map<Any, Int> = emptyMap()
+ }
+ }
+
+ rule.setContent {
+ counter.value // just to trigger recomposition
+ LazyLayout(
+ itemsProvider = itemsProvider,
+ measurePolicy = policy,
+ // this will return a new object everytime causing LazyLayout recomposition
+ // without causing remeasure
+ modifier = Modifier.composed { Modifier }
+ )
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasureCount).isEqualTo(1)
+ counter.value++
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasureCount).isEqualTo(1)
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
index ae44ce4..251138b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
@@ -21,6 +21,7 @@
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.Handle
import androidx.compose.foundation.text.TEST_FONT_FAMILY
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -60,7 +61,6 @@
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.sp
import androidx.compose.ui.unit.width
import androidx.test.espresso.matcher.BoundedMatcher
@@ -87,16 +87,6 @@
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
- private val composeViewAbsolutePos: IntOffset get() {
- // Get the view position on screen
- val positionArray = IntArray(2)
- view.getLocationOnScreen(positionArray)
- return IntOffset(
- positionArray[0],
- positionArray[1]
- )
- }
-
private lateinit var view: View
private val textContent = "Text Demo Text"
@@ -166,7 +156,7 @@
// Long Press "m" in "Demo", and "Demo" should be selected.
createSelectionContainer()
val characterSize = fontSize.toPx()
- val expectedLeftX = fontSize.toDp().times(textContent.indexOf('D')) - 25.dp
+ val expectedLeftX = fontSize.toDp().times(textContent.indexOf('D'))
val expectedLeftY = fontSize.toDp()
val expectedRightX = fontSize.toDp().times(textContent.indexOf('o') + 1)
val expectedRightY = fontSize.toDp()
@@ -188,20 +178,14 @@
times(1)
).performHapticFeedback(HapticFeedbackType.TextHandleMove)
}
- rule.doubleSelectionHandleMatches(
- 0,
- matchesPosition(
- composeViewAbsolutePos.x.toDp() + expectedRightX,
- composeViewAbsolutePos.y.toDp() + expectedRightY
- )
- )
- rule.doubleSelectionHandleMatches(
- 1,
- matchesPosition(
- composeViewAbsolutePos.x.toDp() + expectedLeftX,
- composeViewAbsolutePos.y.toDp() + expectedLeftY
- )
- )
+
+ // Check the position of the anchors of the selection handles. We don't need to compare
+ // to the absolute position since the semantics report selection relative to the
+ // container composable, not the screen.
+ rule.onNode(isSelectionHandle(Handle.SelectionStart))
+ .assertHandlePositionMatches(expectedLeftX, expectedLeftY)
+ rule.onNode(isSelectionHandle(Handle.SelectionEnd))
+ .assertHandlePositionMatches(expectedRightX, expectedRightY)
}
}
@@ -212,8 +196,7 @@
// Long Press "m" in "Demo", and "Demo" should be selected.
createSelectionContainer(isRtl = true)
val characterSize = fontSize.toPx()
- val expectedLeftX =
- rule.rootWidth() - fontSize.toDp().times(textContent.length) - 25.dp
+ val expectedLeftX = rule.rootWidth() - fontSize.toDp().times(textContent.length)
val expectedLeftY = fontSize.toDp()
val expectedRightX = rule.rootWidth() - fontSize.toDp().times(" Demo Text".length)
val expectedRightY = fontSize.toDp()
@@ -239,20 +222,14 @@
times(1)
).performHapticFeedback(HapticFeedbackType.TextHandleMove)
}
- rule.doubleSelectionHandleMatches(
- 0,
- matchesPosition(
- composeViewAbsolutePos.x.toDp() + expectedRightX,
- composeViewAbsolutePos.y.toDp() + expectedRightY
- )
- )
- rule.doubleSelectionHandleMatches(
- 1,
- matchesPosition(
- composeViewAbsolutePos.x.toDp() + expectedLeftX,
- composeViewAbsolutePos.y.toDp() + expectedLeftY
- )
- )
+
+ // Check the position of the anchors of the selection handles. We don't need to compare
+ // to the absolute position since the semantics report selection relative to the
+ // container composable, not the screen.
+ rule.onNode(isSelectionHandle(Handle.SelectionStart))
+ .assertHandlePositionMatches(expectedLeftX, expectedLeftY)
+ rule.onNode(isSelectionHandle(Handle.SelectionEnd))
+ .assertHandlePositionMatches(expectedRightX, expectedRightY)
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt
index 99a3a68..69c23d1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt
@@ -18,13 +18,14 @@
import android.view.View
import android.view.WindowManager
-import androidx.compose.runtime.Composable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.Handle
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView
@@ -32,12 +33,9 @@
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.text.style.ResolvedTextDirection
-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.constrain
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.isPopupLayout
import androidx.test.espresso.Espresso
@@ -55,7 +53,6 @@
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
-import kotlin.math.max
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -76,17 +73,20 @@
y = offset.y
*/
with(rule.density) {
- val expectedPositionX = offset.x.toDp() - HandleWidth
+ val expectedAnchorPositionX = offset.x.toDp()
+ val expectedPopupPositionX = expectedAnchorPositionX - HandleWidth
val expectedPositionY = offset.y.toDp()
createSelectionHandle(isStartHandle = true)
rule.singleSelectionHandleMatches(
matchesPosition(
- composeViewAbsolutePos.x.toDp() + expectedPositionX,
+ composeViewAbsolutePos.x.toDp() + expectedPopupPositionX,
composeViewAbsolutePos.y.toDp() + expectedPositionY
)
)
+ rule.onNode(isSelectionHandle(Handle.SelectionStart))
+ .assertHandlePositionMatches(expectedAnchorPositionX, expectedPositionY)
}
}
@@ -97,17 +97,20 @@
y = offset.y
*/
with(rule.density) {
- val expectedPositionX = offset.x.toDp() - HandleWidth
+ val expectedAnchorPositionX = offset.x.toDp()
+ val expectedPopupPositionX = expectedAnchorPositionX - HandleWidth
val expectedPositionY = offset.y.toDp()
createSelectionHandle(isStartHandle = true, isRtl = true)
rule.singleSelectionHandleMatches(
matchesPosition(
- composeViewAbsolutePos.x.toDp() + expectedPositionX,
+ composeViewAbsolutePos.x.toDp() + expectedPopupPositionX,
composeViewAbsolutePos.y.toDp() + expectedPositionY
)
)
+ rule.onNode(isSelectionHandle(Handle.SelectionStart))
+ .assertHandlePositionMatches(expectedAnchorPositionX, expectedPositionY)
}
}
@@ -129,6 +132,8 @@
composeViewAbsolutePos.y.toDp() + expectedPositionY
)
)
+ rule.onNode(isSelectionHandle(Handle.SelectionEnd))
+ .assertHandlePositionMatches(expectedPositionX, expectedPositionY)
}
}
@@ -149,17 +154,80 @@
composeViewAbsolutePos.y.toDp() + expectedPositionY
)
)
+ rule.onNode(isSelectionHandle(Handle.SelectionEnd))
+ .assertHandlePositionMatches(expectedPositionX, expectedPositionY)
}
}
- private fun createSelectionHandle(isStartHandle: Boolean, isRtl: Boolean = false) {
+ @Test
+ fun leftHandle_semanticsPosition_isRelativeToContainer() {
+ with(rule.density) {
+ val containerOffsetX = 20.dp
+ val containerOffsetY = 30.dp
+ // Since the semantics position is relative to the container, it shouldn't include the
+ // container offset.
+ val expectedSemanticsPositionX = offset.x.toDp()
+ val expectedSemanticsPositionY = offset.y.toDp()
+ // However, the popup position, which is in absolute screen coordinates, should include
+ // the container offset.
+ val expectedPopupPositionX = containerOffsetX + expectedSemanticsPositionX - HandleWidth
+ val expectedPopupPositionY = containerOffsetY + expectedSemanticsPositionY
+
+ createSelectionHandle(
+ isStartHandle = true,
+ containerModifier = Modifier.offset(containerOffsetX, containerOffsetY)
+ )
+
+ rule.singleSelectionHandleMatches(
+ matchesPosition(
+ composeViewAbsolutePos.x.toDp() + expectedPopupPositionX,
+ composeViewAbsolutePos.y.toDp() + expectedPopupPositionY
+ )
+ )
+ rule.onNode(isSelectionHandle(Handle.SelectionStart))
+ .assertHandlePositionMatches(expectedSemanticsPositionX, expectedSemanticsPositionY)
+ }
+ }
+
+ @Test
+ fun rightHandle_semanticsPosition_isRelativeToContainer() {
+ with(rule.density) {
+ val containerOffsetX = 20.dp
+ val containerOffsetY = 30.dp
+ // Since the semantics position is relative to the container, it shouldn't include the
+ // container offset.
+ val expectedSemanticsPositionX = offset.x.toDp()
+ val expectedSemanticsPositionY = offset.y.toDp()
+ // However, the popup position, which is in absolute screen coordinates, should include
+ // the container offset.
+ val expectedPopupPositionX = containerOffsetX + expectedSemanticsPositionX
+ val expectedPopupPositionY = containerOffsetY + expectedSemanticsPositionY
+
+ createSelectionHandle(
+ isStartHandle = false,
+ containerModifier = Modifier.offset(containerOffsetX, containerOffsetY)
+ )
+
+ rule.singleSelectionHandleMatches(
+ matchesPosition(
+ composeViewAbsolutePos.x.toDp() + expectedPopupPositionX,
+ composeViewAbsolutePos.y.toDp() + expectedPopupPositionY
+ )
+ )
+ rule.onNode(isSelectionHandle(Handle.SelectionEnd))
+ .assertHandlePositionMatches(expectedSemanticsPositionX, expectedSemanticsPositionY)
+ }
+ }
+
+ private fun createSelectionHandle(
+ isStartHandle: Boolean,
+ containerModifier: Modifier = Modifier,
+ isRtl: Boolean = false
+ ) {
val measureLatch = CountDownLatch(1)
- with(rule.density) {
- val parentWidthDp = parentSizeWidth
- val parentHeightDp = parentSizeHeight
-
- rule.setContent {
+ rule.setContent {
+ Box(containerModifier) {
// Get the compose view position on screen
val composeView = LocalView.current
val positionArray = IntArray(2)
@@ -174,7 +242,7 @@
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
SimpleLayout {
- SimpleContainer(width = parentWidthDp, height = parentHeightDp) {}
+ Spacer(Modifier.size(parentSizeWidth, parentSizeHeight))
SelectionHandle(
position = offset,
isStartHandle = isStartHandle,
@@ -194,48 +262,48 @@
private fun matchesPosition(expectedPositionX: Dp, expectedPositionY: Dp):
BoundedMatcher<View, View> {
- return object : BoundedMatcher<View, View>(View::class.java) {
- // (-1, -1) no position found
- var positionFound = IntOffset(-1, -1)
+ return object : BoundedMatcher<View, View>(View::class.java) {
+ // (-1, -1) no position found
+ var positionFound = IntOffset(-1, -1)
- override fun matchesSafely(item: View?): Boolean {
- with(rule.density) {
- val position = IntArray(2)
- item?.getLocationOnScreen(position)
- positionFound = IntOffset(position[0], position[1])
+ override fun matchesSafely(item: View?): Boolean {
+ with(rule.density) {
+ val position = IntArray(2)
+ item?.getLocationOnScreen(position)
+ positionFound = IntOffset(position[0], position[1])
- val expectedPositionXInt = expectedPositionX.value.toInt()
- val expectedPositionYInt = expectedPositionY.value.toInt()
- val positionFoundXInt = positionFound.x.toDp().value.toInt()
- val positionFoundYInt = positionFound.y.toDp().value.toInt()
- return expectedPositionXInt == positionFoundXInt &&
- expectedPositionYInt == positionFoundYInt
- }
+ val expectedPositionXInt = expectedPositionX.value.toInt()
+ val expectedPositionYInt = expectedPositionY.value.toInt()
+ val positionFoundXInt = positionFound.x.toDp().value.toInt()
+ val positionFoundYInt = positionFound.y.toDp().value.toInt()
+ return expectedPositionXInt == positionFoundXInt &&
+ expectedPositionYInt == positionFoundYInt
}
+ }
- override fun describeTo(description: Description?) {
- with(rule.density) {
- description?.appendText(
- "with expected position: " +
- "${expectedPositionX.value}, ${expectedPositionY.value} " +
- "but position found:" +
- "${positionFound.x.toDp().value}, ${positionFound.y.toDp().value}"
- )
- }
+ override fun describeTo(description: Description?) {
+ with(rule.density) {
+ description?.appendText(
+ "with expected position: " +
+ "${expectedPositionX.value}, ${expectedPositionY.value} " +
+ "but position found:" +
+ "${positionFound.x.toDp().value}, ${positionFound.y.toDp().value}"
+ )
}
}
}
+ }
}
-internal fun ComposeTestRule.singleSelectionHandleMatches(viewMatcher: Matcher<in View>) {
+private fun ComposeTestRule.singleSelectionHandleMatches(viewMatcher: Matcher<in View>) {
// Make sure that current measurement/drawing is finished
- runOnIdle { }
+ waitForIdle()
Espresso.onView(CoreMatchers.instanceOf(ViewRootForTest::class.java))
.inRoot(SingleSelectionHandleMatcher())
.check(ViewAssertions.matches(viewMatcher))
}
-internal class SingleSelectionHandleMatcher : TypeSafeMatcher<Root>() {
+private class SingleSelectionHandleMatcher : TypeSafeMatcher<Root>() {
var lastSeenWindowParams: WindowManager.LayoutParams? = null
@@ -250,61 +318,4 @@
}
return matches
}
-}
-
-/**
- * A Container Box implementation used for selection children and handle layout
- */
-@Composable
-internal fun SimpleContainer(
- modifier: Modifier = Modifier,
- width: Dp? = null,
- height: Dp? = null,
- content: @Composable () -> Unit
-) {
- Layout(content, modifier) { measurables, incomingConstraints ->
- val containerConstraints =
- incomingConstraints.constrain(
- Constraints().copy(
- width?.roundToPx() ?: 0,
- width?.roundToPx() ?: Constraints.Infinity,
- height?.roundToPx() ?: 0,
- height?.roundToPx() ?: Constraints.Infinity
- )
- )
- val childConstraints = containerConstraints.copy(minWidth = 0, minHeight = 0)
- var placeable: Placeable? = null
- val containerWidth = if (
- containerConstraints.hasFixedWidth
- ) {
- containerConstraints.maxWidth
- } else {
- placeable = measurables.firstOrNull()?.measure(childConstraints)
- max((placeable?.width ?: 0), containerConstraints.minWidth)
- }
- val containerHeight = if (
- containerConstraints.hasFixedHeight
- ) {
- containerConstraints.maxHeight
- } else {
- if (placeable == null) {
- placeable = measurables.firstOrNull()?.measure(childConstraints)
- }
- max((placeable?.height ?: 0), containerConstraints.minHeight)
- }
- layout(containerWidth, containerHeight) {
- val p = placeable ?: measurables.firstOrNull()?.measure(childConstraints)
- p?.let {
- val position = Alignment.Center.align(
- IntSize(it.width, it.height),
- IntSize(containerWidth, containerHeight),
- layoutDirection
- )
- it.placeRelative(
- position.x,
- position.y
- )
- }
- }
- }
-}
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
index 119d458..3a4a843 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
@@ -16,41 +16,46 @@
package androidx.compose.foundation.text.selection
-import android.view.View
-import androidx.compose.ui.platform.ViewRootForTest
-import androidx.compose.ui.test.junit4.ComposeTestRule
-import androidx.compose.ui.window.isPopupLayout
-import androidx.test.espresso.Espresso
-import androidx.test.espresso.Root
-import androidx.test.espresso.assertion.ViewAssertions
-import org.hamcrest.CoreMatchers
-import org.hamcrest.Description
-import org.hamcrest.Matcher
-import org.hamcrest.TypeSafeMatcher
+import androidx.compose.foundation.text.Handle
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.unit.Dp
+import com.google.common.truth.Truth.assertWithMessage
-internal fun ComposeTestRule.doubleSelectionHandleMatches(
- index: Int,
- viewMatcher: Matcher<in View>
-) {
- // Make sure that current measurement/drawing is finished
- runOnIdle { }
- Espresso.onView(CoreMatchers.instanceOf(ViewRootForTest::class.java))
- .inRoot(DoubleSelectionHandleMatcher(index))
- .check(ViewAssertions.matches(viewMatcher))
-}
-
-internal class DoubleSelectionHandleMatcher(val index: Int) : TypeSafeMatcher<Root>() {
- var popupsMatchedSoFar: Int = 0
-
- override fun describeTo(description: Description?) {
- description?.appendText("DoubleSelectionHandleMatcher")
+/**
+ * Matches selection handles by looking for the [SelectionHandleInfoKey] property that has a
+ * [SelectionHandleInfo] with the given [handle]. If [handle] is null (the default), then all
+ * handles are matched.
+ */
+internal fun isSelectionHandle(handle: Handle? = null) =
+ SemanticsMatcher("is ${handle ?: "any"} handle") { node ->
+ if (handle == null) {
+ SelectionHandleInfoKey in node.config
+ } else {
+ node.config.getOrNull(SelectionHandleInfoKey)?.handle == handle
+ }
}
- override fun matchesSafely(item: Root?): Boolean {
- val matches = item != null && isPopupLayout(item.decorView)
- if (matches) {
- popupsMatchedSoFar++
- }
- return matches && popupsMatchedSoFar == index + 1
+/**
+ * Asserts about the [SelectionHandleInfo.position] for the matching node. This is the position of
+ * the handle's _anchor_, not the position of the popup itself. E.g. for a cursor handle this is the
+ * position of the bottom of the cursor, which will be in the center of the popup.
+ */
+internal fun SemanticsNodeInteraction.assertHandlePositionMatches(
+ expectedX: Dp,
+ expectedY: Dp
+) {
+ val node = fetchSemanticsNode()
+ with(node.layoutInfo.density) {
+ val positionFound = node.config[SelectionHandleInfoKey].position
+ val positionFoundX = positionFound.x.toDp()
+ val positionFoundY = positionFound.y.toDp()
+ val message = "Expected position ($expectedX, $expectedY), " +
+ "but found ($positionFoundX, $positionFoundY)"
+ assertWithMessage(message).that(positionFoundX.value)
+ .isWithin(5f).of(expectedX.value)
+ assertWithMessage(message).that(positionFoundY.value)
+ .isWithin(5f).of(expectedY.value)
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
index 190322c..a6da521 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
@@ -46,7 +46,7 @@
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
import org.junit.Rule
import org.junit.Test
@@ -246,6 +246,29 @@
}
@Test
+ fun textField_onValueChangeRecomposeTest() {
+ // sample code in b/200577798
+ val value = mutableStateOf(TextFieldValue(""))
+ var lastNewValue: TextFieldValue? = null
+ val onValueChange: (TextFieldValue) -> Unit = { newValue ->
+ lastNewValue = newValue
+ if (newValue.text.isBlank() || newValue.text.startsWith("z")) {
+ value.value = newValue
+ }
+ }
+
+ keysSequenceTest(value = value, {
+ // based on repro steps in the ticket, one of the values would become "aa"
+ // check 10 times to make sure it is not "aa"
+ repeat(10) {
+ Key.A.downAndUp()
+ // should always be "a" and buffer should not accumulate
+ assertThat(lastNewValue?.text).isEqualTo("a")
+ }
+ }
+ }
+
+ @Test
fun textField_pageNavigation() {
keysSequenceTest(
initText = "1\n2\n3\n4\n5",
@@ -275,13 +298,13 @@
fun expectedText(text: String) {
rule.runOnIdle {
- Truth.assertThat(state.value.text).isEqualTo(text)
+ assertThat(state.value.text).isEqualTo(text)
}
}
fun expectedSelection(selection: TextRange) {
rule.runOnIdle {
- Truth.assertThat(state.value.selection).isEqualTo(selection)
+ assertThat(state.value.selection).isEqualTo(selection)
}
}
}
@@ -289,33 +312,39 @@
private fun keysSequenceTest(
initText: String = "",
modifier: Modifier = Modifier.fillMaxSize(),
- sequence: SequenceScope.() -> Unit
+ sequence: SequenceScope.() -> Unit,
+ ) {
+ val value = mutableStateOf(TextFieldValue(initText))
+ keysSequenceTest(value = value, modifier = modifier, sequence = sequence)
+ }
+
+ private fun keysSequenceTest(
+ value: MutableState<TextFieldValue>,
+ modifier: Modifier = Modifier.fillMaxSize(),
+ onValueChange: (TextFieldValue) -> Unit = { value.value = it },
+ sequence: SequenceScope.() -> Unit,
) {
val inputService = TextInputService(mock())
-
- val state = mutableStateOf(TextFieldValue(initText))
val focusFequester = FocusRequester()
rule.setContent {
CompositionLocalProvider(
LocalTextInputService provides inputService
) {
BasicTextField(
- value = state.value,
+ value = value.value,
textStyle = TextStyle(
fontFamily = TEST_FONT_FAMILY,
fontSize = 10.sp
),
modifier = modifier.focusRequester(focusFequester),
- >
- state.value = it
- }
+ >
)
}
}
rule.runOnIdle { focusFequester.requestFocus() }
- sequence(SequenceScope(state) { rule.onNode(hasSetTextAction()) })
+ sequence(SequenceScope(value) { rule.onNode(hasSetTextAction()) })
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
index ff02ea8..50b289d 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
@@ -30,9 +30,11 @@
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.Handle
import androidx.compose.foundation.text.TextFieldScrollerPosition
import androidx.compose.foundation.text.TextLayoutResultProxy
import androidx.compose.foundation.text.maxLinesHeight
+import androidx.compose.foundation.text.selection.isSelectionHandle
import androidx.compose.foundation.text.textFieldScroll
import androidx.compose.foundation.text.textFieldScrollable
import androidx.compose.foundation.verticalScroll
@@ -576,10 +578,7 @@
rule.onNodeWithTag(tag)
.performClick()
- // Check that handle is displayed (if it's not, we can't check if it gets hidden).
- rule.onNode(isPopup())
- .assertIsDisplayed()
- // TODO(b/139861182) Assert handle position is within field bounds?
+ rule.onNode(isSelectionHandle(Handle.Cursor)).assertIsDisplayed()
// Scroll up by twice the height to move the cursor out of the visible area.
rule.onNodeWithTag(tag)
@@ -589,7 +588,7 @@
}
// Check that cursor is hidden.
- rule.onNode(isPopup()).assertDoesNotExist()
+ rule.onAllNodes(isSelectionHandle()).assertCountEquals(0)
// Scroll back and make sure the handles are shown again.
rule.onNodeWithTag(tag)
@@ -597,7 +596,7 @@
moveBy(Offset(x = 0f, y = size * 2f))
}
- rule.onNode(isPopup()).assertIsDisplayed()
+ rule.onNode(isSelectionHandle(Handle.Cursor)).assertIsDisplayed()
}
@OptIn(ExperimentalTestApi::class)
@@ -606,19 +605,6 @@
val size = 200
val tag = "Text"
- fun assertHandlesDisplayed() {
- rule.onAllNodes(isPopup())
- .assertCountEquals(2)
- .apply {
- (0 until 2)
- .map(::get)
- .forEach {
- it.assertIsDisplayed()
- // TODO(b/139861182) Assert handle position is within field bounds?
- }
- }
- }
-
with(rule.density) {
rule.setContent {
BasicTextField(
@@ -644,7 +630,8 @@
}
// Check that both handles are displayed (if not, we can't check that they get hidden).
- assertHandlesDisplayed()
+ rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
+ rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
// Scroll up by twice the height to move the cursor out of the visible area.
rule.onNodeWithTag(tag)
@@ -663,7 +650,8 @@
moveBy(Offset(x = 0f, y = size * 2f))
}
- assertHandlesDisplayed()
+ rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
+ rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
}
private fun ComposeContentTestRule.setupHorizontallyScrollableContent(
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
index 7b9119d..2e36f5e 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
@@ -18,6 +18,10 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.Handle
+import androidx.compose.foundation.text.selection.HandleReferencePoint.TopLeft
+import androidx.compose.foundation.text.selection.HandleReferencePoint.TopMiddle
+import androidx.compose.foundation.text.selection.HandleReferencePoint.TopRight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -34,6 +38,7 @@
import androidx.compose.ui.graphics.ImageBitmapConfig
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
@@ -61,10 +66,21 @@
} else {
HandleReferencePoint.TopLeft
}
+
HandlePopup(position = position, handleReferencePoint = handleReferencePoint) {
if (content == null) {
DefaultSelectionHandle(
- modifier = modifier,
+ modifier = modifier
+ .semantics {
+ this[SelectionHandleInfoKey] = SelectionHandleInfo(
+ handle = if (isStartHandle) {
+ Handle.SelectionStart
+ } else {
+ Handle.SelectionEnd
+ },
+ position = position
+ )
+ },
isStartHandle = isStartHandle,
direction = direction,
handlesCrossed = handlesCrossed
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
index 4ad43a3..10add68 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
@@ -43,23 +43,28 @@
SubcomposeLayout(
subcomposeLayoutState,
- modifier.then(state.remeasurementModifier)
- ) { constraints ->
- itemContentFactory.onBeforeMeasure(this, constraints)
+ modifier.then(state.remeasurementModifier),
+ remember(itemContentFactory, state, measurePolicy) {
+ { constraints ->
+ itemContentFactory.onBeforeMeasure(this, constraints)
- val placeablesProvider = LazyLayoutPlaceablesProvider(
- state.itemsProvider(),
- itemContentFactory,
- this
- )
- val measureResult = with(measurePolicy) { measure(placeablesProvider, constraints) }
+ val placeablesProvider = LazyLayoutPlaceablesProvider(
+ state.itemsProvider(),
+ itemContentFactory,
+ this
+ )
+ val measureResult = with(measurePolicy) { measure(placeablesProvider, constraints) }
- state.onPostMeasureListener?.apply { onPostMeasure(measureResult, placeablesProvider) }
- state.layoutInfoState.value = measureResult
- state.layoutInfoNonObservable = measureResult
+ state.onPostMeasureListener?.apply {
+ onPostMeasure(measureResult, placeablesProvider)
+ }
+ state.layoutInfoState.value = measureResult
+ state.layoutInfoNonObservable = measureResult
- measureResult
- }
+ measureResult
+ }
+ }
+ )
}
private const val MaxItemsToRetainForReuse = 2
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 68083e0..74dd799 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
@@ -21,6 +21,8 @@
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
+import androidx.compose.foundation.text.selection.SelectionHandleInfo
+import androidx.compose.foundation.text.selection.SelectionHandleInfoKey
import androidx.compose.foundation.text.selection.SimpleLayout
import androidx.compose.foundation.text.selection.TextFieldSelectionHandle
import androidx.compose.foundation.text.selection.TextFieldSelectionManager
@@ -486,6 +488,7 @@
state = state,
manager = manager,
value = value,
+ >
editable = !readOnly,
singleLine = maxLines == 1,
offsetMapping = offsetMapping,
@@ -878,9 +881,16 @@
val position = manager.getCursorPosition(LocalDensity.current)
CursorHandle(
handlePosition = position,
- modifier = Modifier.pointerInput(observer) {
- detectDragGesturesWithObserver(observer)
- },
+ modifier = Modifier
+ .pointerInput(observer) {
+ detectDragGesturesWithObserver(observer)
+ }
+ .semantics {
+ this[SelectionHandleInfoKey] = SelectionHandleInfo(
+ handle = Handle.Cursor,
+ position = position
+ )
+ },
content = null
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
index c4217c8..69751f3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
@@ -56,6 +56,7 @@
val offsetMapping: OffsetMapping = OffsetMapping.Identity,
val undoManager: UndoManager? = null,
private val keyMapping: KeyMapping = platformDefaultKeyMapping,
+ private val onValueChange: (TextFieldValue) -> Unit = {}
) {
private fun List<EditCommand>.apply() {
val newTextFieldValue = state.processor.apply(
@@ -63,12 +64,8 @@
add(0, FinishComposingTextCommand())
}
)
- @OptIn(InternalFoundationTextApi::class)
- if (newTextFieldValue.annotatedString.text != state.textDelegate.text.text) {
- // Text has been changed, enter the HandleState.None and hide the cursor handle.
- state.handleState = HandleState.None
- }
- state.onValueChange(newTextFieldValue)
+
+ onValueChange(newTextFieldValue)
}
private fun EditCommand.apply() {
@@ -192,10 +189,10 @@
KeyCommand.DESELECT -> deselect()
KeyCommand.UNDO -> {
undoManager?.makeSnapshot(value)
- undoManager?.undo()?.let { this@TextFieldKeyInput.state.onValueChange(it) }
+ undoManager?.undo()?.let { this@TextFieldKeyInput.onValueChange(it) }
}
KeyCommand.REDO -> {
- undoManager?.redo()?.let { this@TextFieldKeyInput.state.onValueChange(it) }
+ undoManager?.redo()?.let { this@TextFieldKeyInput.onValueChange(it) }
}
KeyCommand.CHARACTER_PALETTE -> { showCharacterPalette() }
}
@@ -215,7 +212,7 @@
if (preparedSelection.selection != value.selection ||
preparedSelection.annotatedString != value.annotatedString
) {
- state.onValueChange(preparedSelection.value)
+ onValueChange(preparedSelection.value)
}
}
}
@@ -225,6 +222,7 @@
state: TextFieldState,
manager: TextFieldSelectionManager,
value: TextFieldValue,
+ onValueChange: (TextFieldValue) -> Unit = {},
editable: Boolean,
singleLine: Boolean,
offsetMapping: OffsetMapping,
@@ -239,7 +237,8 @@
singleLine = singleLine,
offsetMapping = offsetMapping,
preparedSelectionState = preparedSelectionState,
- undoManager = undoManager
+ undoManager = undoManager,
+ >
)
Modifier.onKeyEvent(processor::process)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt
index 157ac29..97a367d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt
@@ -16,15 +16,37 @@
package androidx.compose.foundation.text.selection
+import androidx.compose.foundation.text.Handle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.unit.dp
internal val HandleWidth = 25.dp
internal val HandleHeight = 25.dp
+/**
+ * [SelectionHandleInfo]s for the nodes representing selection handles. These nodes are in popup
+ * windows, and will respond to drag gestures.
+ */
+internal val SelectionHandleInfoKey =
+ SemanticsPropertyKey<SelectionHandleInfo>("SelectionHandleInfo")
+
+/**
+ * Information about a single selection handle popup.
+ *
+ * @param handle Which selection [Handle] this is about.
+ * @param position The position that the handle is anchored to relative to the selectable content.
+ * This position is not necessarily the position of the popup itself, it's the position that the
+ * handle "points" to (so e.g. top-middle for [Handle.Cursor]).
+ */
+internal data class SelectionHandleInfo(
+ val handle: Handle,
+ val position: Offset
+)
+
@Composable
internal expect fun SelectionHandle(
position: Offset,
diff --git a/core/core-ktx/src/main/java/androidx/core/os/PersistableBundle.kt b/core/core-ktx/src/main/java/androidx/core/os/PersistableBundle.kt
index 72c542f..744276e 100644
--- a/core/core-ktx/src/main/java/androidx/core/os/PersistableBundle.kt
+++ b/core/core-ktx/src/main/java/androidx/core/os/PersistableBundle.kt
@@ -31,8 +31,10 @@
*/
@RequiresApi(21)
fun persistableBundleOf(vararg pairs: Pair<String, Any?>): PersistableBundle {
- val persistableBundle = Api21Impl.createPersistableBundle(pairs.size)
- pairs.forEach { (key, value) -> Api21Impl.putValue(persistableBundle, key, value) }
+ val persistableBundle = PersistableBundleApi21ImplKt.createPersistableBundle(pairs.size)
+ pairs.forEach { (key, value) ->
+ PersistableBundleApi21ImplKt.putValue(persistableBundle, key, value)
+ }
return persistableBundle
}
@@ -46,17 +48,20 @@
*/
@RequiresApi(21)
fun Map<String, Any?>.toPersistableBundle(): PersistableBundle {
- val persistableBundle = Api21Impl.createPersistableBundle(this.size)
+ val persistableBundle = PersistableBundleApi21ImplKt.createPersistableBundle(this.size)
for ((key, value) in this) {
- Api21Impl.putValue(persistableBundle, key, value)
+ PersistableBundleApi21ImplKt.putValue(persistableBundle, key, value)
}
return persistableBundle
}
+// These classes ends up being top-level even though they're private. The PersistableBundle prefix
+// helps prevent clashes with other ApiImpls in androidx.core.os. And the Kt suffix is used by
+// Jetifier to keep them grouped with other members of the core-ktx module.
@RequiresApi(21)
-private object Api21Impl {
+private object PersistableBundleApi21ImplKt {
@DoNotInline
@JvmStatic
fun createPersistableBundle(capacity: Int): PersistableBundle = PersistableBundle(capacity)
@@ -71,7 +76,7 @@
// Scalars
is Boolean -> {
if (Build.VERSION.SDK_INT >= 22) {
- Api22Impl.putBoolean(this, key, value)
+ PersistableBundleApi22ImplKt.putBoolean(this, key, value)
} else {
throw IllegalArgumentException(
"Illegal value type boolean for key \"$key\""
@@ -88,7 +93,7 @@
// Scalar arrays
is BooleanArray -> {
if (Build.VERSION.SDK_INT >= 22) {
- Api22Impl.putBooleanArray(this, key, value)
+ PersistableBundleApi22ImplKt.putBooleanArray(this, key, value)
} else {
throw IllegalArgumentException(
"Illegal value type boolean[] for key \"$key\""
@@ -126,7 +131,7 @@
}
@RequiresApi(22)
-private object Api22Impl {
+private object PersistableBundleApi22ImplKt {
@DoNotInline
@JvmStatic
fun putBoolean(persistableBundle: PersistableBundle, key: String?, value: Boolean) {