Merge "Revert "Remove graphicsLayer from BasicText"" into androidx-main
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 8769836..097d831 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -1,14 +1,18 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
- <inspection_tool class="AndroidLintKotlinPropertyAccess" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
- <inspection_tool class="AndroidLintLambdaLast" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
- <inspection_tool class="AndroidLintNoHardKeywords" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
- <inspection_tool class="AndroidLintSyntheticAccessor" enabled="true" level="WARNING" enabled_by_default="true">
- <scope name="Compose" level="WARNING" enabled="false" />
- <scope name="buildSrc" level="WARNING" enabled="false" />
+ <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
+ <option name="composableFile" value="true" />
</inspection_tool>
- <inspection_tool class="AndroidLintUnknownNullness" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
+ <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
<inspection_tool class="DeprecatedIsStillUsed" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="Deprecation" enabled="true" level="WARNING" enabled_by_default="true">
<option name="IGNORE_IMPORT_STATEMENTS" value="false" />
@@ -18,6 +22,18 @@
<option name="namePattern" value="[A-Za-z\d]+" />
</scope>
</inspection_tool>
+ <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
<inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
<option name="TOP_LEVEL_CLASS_OPTIONS">
<value>
@@ -49,11 +65,47 @@
<option name="IGNORE_POINT_TO_ITSELF" value="false" />
<option name="myAdditionalJavadocTags" value="hide" />
</inspection_tool>
+ <inspection_tool class="JavadocDeclaration" enabled="true" level="WARNING" enabled_by_default="true">
+ <option name="ADDITIONAL_TAGS" value="hide" />
+ </inspection_tool>
<inspection_tool class="JavadocReference" enabled="true" level="ERROR" enabled_by_default="true">
<option name="REPORT_INACCESSIBLE" value="false" />
</inspection_tool>
<inspection_tool class="KDocUnresolvedReference" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="MissingDeprecatedAnnotation" enabled="true" level="ERROR" enabled_by_default="true" />
+ <inspection_tool class="MissingJavadoc" enabled="true" level="WARNING" enabled_by_default="true">
+ <option name="PACKAGE_SETTINGS">
+ <Options>
+ <option name="ENABLED" value="false" />
+ </Options>
+ </option>
+ <option name="MODULE_SETTINGS">
+ <Options>
+ <option name="ENABLED" value="false" />
+ </Options>
+ </option>
+ <option name="TOP_LEVEL_CLASS_SETTINGS">
+ <Options>
+ <option name="ENABLED" value="false" />
+ </Options>
+ </option>
+ <option name="INNER_CLASS_SETTINGS">
+ <Options>
+ <option name="ENABLED" value="false" />
+ </Options>
+ </option>
+ <option name="METHOD_SETTINGS">
+ <Options>
+ <option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
+ <option name="ENABLED" value="false" />
+ </Options>
+ </option>
+ <option name="FIELD_SETTINGS">
+ <Options>
+ <option name="ENABLED" value="false" />
+ </Options>
+ </option>
+ </inspection_tool>
<inspection_tool class="NullableProblems" enabled="true" level="ERROR" enabled_by_default="true">
<option name="REPORT_NULLABLE_METHOD_OVERRIDES_NOTNULL" value="true" />
<option name="REPORT_NOT_ANNOTATED_METHOD_OVERRIDES_NOTNULL" value="true" />
@@ -65,6 +117,21 @@
<option name="REPORT_NULLS_PASSED_TO_NON_ANNOTATED_METHOD" value="true" />
<option name="REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER" value="false" />
</inspection_tool>
+ <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
<inspection_tool class="PrivatePropertyName" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<scope name="Compose" level="WEAK WARNING" enabled="true">
<option name="namePattern" value="_?[A-Za-z\d]+" />
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Insight.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Insight.kt
new file mode 100644
index 0000000..76f5162
--- /dev/null
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Insight.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 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.benchmark
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Represents an insight into performance issues detected during a benchmark.
+ *
+ * Provides details about the specific criterion that was violated, along with information about
+ * where and how the violation was observed.
+ *
+ * @param criterion A description of the performance issue, including the expected behavior and any
+ * relevant thresholds.
+ * @param observed Specific details about when and how the violation occurred, such as the
+ * iterations where it was observed and any associated values.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // TODO(364598145): generalise
+data class Insight(val criterion: String, val observed: String)
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
index e2b8b01..b5e1574 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
@@ -20,6 +20,7 @@
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.test.platform.app.InstrumentationRegistry
+import java.lang.StringBuilder
import java.util.Locale
import org.jetbrains.annotations.TestOnly
@@ -63,7 +64,8 @@
message: String? = null,
measurements: Measurements? = null,
iterationTracePaths: List<String>? = null,
- profilerResults: List<Profiler.ResultFile> = emptyList()
+ profilerResults: List<Profiler.ResultFile> = emptyList(),
+ insights: List<Insight> = emptyList(),
) {
if (warningMessage != null) {
InstrumentationResults.scheduleIdeWarningOnNextReport(warningMessage)
@@ -74,7 +76,8 @@
message = message,
measurements = measurements,
iterationTracePaths = iterationTracePaths,
- profilerResults = profilerResults
+ profilerResults = profilerResults,
+ insights = insights
)
reportIdeSummary(summaryV1 = summaryPair.summaryV1, summaryV2 = summaryPair.summaryV2)
}
@@ -168,7 +171,8 @@
message: String? = null,
measurements: Measurements? = null,
iterationTracePaths: List<String>? = null,
- profilerResults: List<Profiler.ResultFile> = emptyList()
+ profilerResults: List<Profiler.ResultFile> = emptyList(),
+ insights: List<Insight> = emptyList(),
): IdeSummaryPair {
val warningMessage = ideWarningPrefix.ifEmpty { null }
ideWarningPrefix = ""
@@ -178,7 +182,7 @@
val linkableIterTraces =
iterationTracePaths?.map { absolutePath ->
Outputs.relativePathFor(absolutePath).replace("(", "\\(").replace(")", "\\)")
- }
+ } ?: emptyList()
if (measurements != null) {
require(measurements.isNotEmpty()) { "Require non-empty list of metric results." }
@@ -248,7 +252,7 @@
" $name min $min, median $median, max $max"
}
v2metricLines =
- if (linkableIterTraces != null) {
+ if (linkableIterTraces.isNotEmpty()) {
// Per iteration trace paths present, so link min/med/max to respective
// iteration traces
metricLines { name, min, median, max, result ->
@@ -267,34 +271,72 @@
v2metricLines = emptyList()
}
- val v2traceLinks =
- if (linkableIterTraces != null) {
- listOf(
- " Traces: Iteration " +
- linkableIterTraces
- .mapIndexed { index, path -> "[$index](file://$path)" }
- .joinToString(" ")
- )
+ fun markdownFileLink(label: String, outputRelativePath: String): String =
+ "[$label](file://$outputRelativePath)"
+
+ // TODO(353692849): split into methods and remove the v1 format (replace with a v2 getter)
+ val v2lines =
+ if (insights.isEmpty()) {
+ val v2traceLinks =
+ if (linkableIterTraces.isNotEmpty()) {
+ listOf(
+ " Traces: Iteration " +
+ linkableIterTraces
+ .mapIndexed { index, path -> markdownFileLink("$index", path) }
+ .joinToString(" ")
+ )
+ } else {
+ emptyList()
+ } +
+ profilerResults.map {
+ " ${markdownFileLink(it.label, it.sanitizedOutputRelativePath)}"
+ }
+ listOfNotNull(warningMessage, testName, message) +
+ v2metricLines +
+ v2traceLinks +
+ "" /* adds \n */
} else {
- emptyList()
- } +
- profilerResults.map {
- " [${it.label}](file://${it.sanitizedOutputRelativePath})"
+ buildList {
+ if (warningMessage != null) add(warningMessage)
+ if (testName != null) add(testName)
+ if (message != null) add(message)
+ val tree = TreeBuilder()
+ if (v2metricLines.isNotEmpty()) {
+ tree.append("Metrics", 0)
+ for (metric in v2metricLines) tree.append(metric, 1)
+ }
+ if (insights.isNotEmpty()) {
+ tree.append("App Startup Insights", 0)
+ for ((criterion, observed) in insights) {
+ tree.append(criterion, 1)
+ tree.append(observed, 2)
+ }
+ }
+ if (linkableIterTraces.isNotEmpty() || profilerResults.isNotEmpty()) {
+ tree.append("Traces", 0)
+ if (linkableIterTraces.isNotEmpty())
+ tree.append(
+ linkableIterTraces
+ .mapIndexed { ix, trace -> markdownFileLink("$ix", trace) }
+ .joinToString(prefix = "Iteration ", separator = " "),
+ 1
+ )
+ for (line in profilerResults) tree.append(
+ markdownFileLink(line.label, line.sanitizedOutputRelativePath),
+ 1
+ )
+ }
+ addAll(tree.build())
+ add("")
}
- return IdeSummaryPair(
- v1lines =
- listOfNotNull(
- warningMessage,
- testName,
- message,
- ) + v1metricLines + /* adds \n */ "",
- v2lines =
- listOfNotNull(
- warningMessage,
- testName,
- message,
- ) + v2metricLines + v2traceLinks + /* adds \n */ ""
- )
+ }
+
+ // TODO(353692849): replace the v1 format with a v2 format regardless insights
+ val v1lines =
+ if (insights.isNotEmpty()) v2lines
+ else listOfNotNull(warningMessage, testName, message) + v1metricLines + /* adds \n */ ""
+
+ return IdeSummaryPair(v1lines = v1lines, v2lines = v2lines)
}
/**
@@ -340,3 +382,34 @@
InstrumentationRegistry.getInstrumentation().sendStatus(2, bundle)
}
}
+
+/**
+ * Constructs a hierarchical, tree-like representation of data, similar to the output of the 'tree'
+ * command.
+ */
+private class TreeBuilder {
+ private val lines = mutableListOf<StringBuilder>()
+ private val nbsp = '\u00A0'
+
+ fun append(message: String, depth: Int): TreeBuilder {
+ require(depth >= 0)
+
+ // Create a new line for the tree node, with appropriate indentation using spaces.
+ val line = StringBuilder()
+ repeat(depth * 4) { line.append(nbsp) }
+ line.append("└── ")
+ line.append(message)
+ lines.add(line)
+
+ // Update vertical lines (pipes) to visually connect the new node to its parent/sibling.
+ // TODO: Optimize this for deep trees to avoid potential quadratic time complexity.
+ val anchorColumn = depth * 4
+ var i = lines.lastIndex - 1 // start climbing with the first line above the newly added one
+ while (i >= 0 && lines[i].getOrNull(anchorColumn) == nbsp) lines[i--][anchorColumn] = '│'
+ if (i >= 0 && lines[i].getOrNull(anchorColumn) == '└') lines[i][anchorColumn] = '├'
+
+ return this
+ }
+
+ fun build(): List<String> = lines.map { it.toString() }
+}
diff --git a/benchmark/benchmark-macro-junit4/build.gradle b/benchmark/benchmark-macro-junit4/build.gradle
index 6a958d6..508a6da 100644
--- a/benchmark/benchmark-macro-junit4/build.gradle
+++ b/benchmark/benchmark-macro-junit4/build.gradle
@@ -71,9 +71,10 @@
kotlinOptions {
// Enable using experimental APIs from within same version group
freeCompilerArgs += [
+ "-opt-in=androidx.benchmark.ExperimentalBenchmarkConfigApi",
"-opt-in=androidx.benchmark.macro.ExperimentalMetricApi",
+ "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi",
"-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi",
- "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi"
]
}
}
diff --git a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
index d3dd114..7825418 100644
--- a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
+++ b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
@@ -115,7 +115,7 @@
compilationMode = compilationMode,
iterations = iterations,
startupMode = startupMode,
- perfettoConfig = null,
+ experimentalConfig = null,
setupBlock = setupBlock,
measureBlock = measureBlock
)
@@ -183,7 +183,7 @@
metrics = metrics,
compilationMode = compilationMode,
iterations = iterations,
- perfettoConfig = experimentalConfig.perfettoConfig,
+ experimentalConfig = experimentalConfig,
startupMode = startupMode,
setupBlock = setupBlock,
measureBlock = measureBlock
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
index b41a083..49689d6 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
@@ -20,6 +20,8 @@
import android.content.Intent
import androidx.annotation.RequiresApi
import androidx.benchmark.DeviceInfo
+import androidx.benchmark.ExperimentalBenchmarkConfigApi
+import androidx.benchmark.ExperimentalConfig
import androidx.benchmark.json.BenchmarkData
import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoHelper
@@ -39,7 +41,7 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
-@OptIn(ExperimentalMacrobenchmarkApi::class)
+@OptIn(ExperimentalMacrobenchmarkApi::class, ExperimentalBenchmarkConfigApi::class)
class MacrobenchmarkTest {
@Before
@@ -60,7 +62,7 @@
compilationMode = CompilationMode.Ignore(),
iterations = 1,
startupMode = null,
- perfettoConfig = null,
+ experimentalConfig = null,
setupBlock = {},
measureBlock = {}
)
@@ -81,7 +83,7 @@
compilationMode = CompilationMode.Ignore(),
iterations = 0, // invalid
startupMode = null,
- perfettoConfig = null,
+ experimentalConfig = null,
setupBlock = {},
measureBlock = {}
)
@@ -101,7 +103,7 @@
compilationMode = CompilationMode.Ignore(),
iterations = 1,
startupMode = StartupMode.COLD,
- perfettoConfig = null,
+ experimentalConfig = null,
setupBlock = {},
measureBlock = {
startActivityAndWait(
@@ -142,7 +144,7 @@
compilationMode = CompilationMode.DEFAULT,
iterations = 2,
startupMode = startupMode,
- perfettoConfig = null,
+ experimentalConfig = null,
setupBlock = {
opOrder += Block.Setup
setupIterations += iteration
@@ -218,7 +220,7 @@
compilationMode = CompilationMode.DEFAULT,
iterations = 3,
startupMode = null,
- perfettoConfig = PerfettoConfig.MinimalTest(atraceApps),
+ experimentalConfig = ExperimentalConfig(PerfettoConfig.MinimalTest(atraceApps)),
setupBlock = {},
measureBlock = { trace(TRACE_LABEL) { Thread.sleep(2) } }
)
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/InsightExtensions.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/InsightExtensions.kt
new file mode 100644
index 0000000..7bca88d
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/InsightExtensions.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.benchmark.macro
+
+import androidx.benchmark.Insight
+import perfetto.protos.AndroidStartupMetric.SlowStartReason
+import perfetto.protos.AndroidStartupMetric.ThresholdValue.ThresholdUnit
+
+/**
+ * Aggregates raw SlowStartReason results into a list of [Insight]s - in a format easier to display
+ * in the IDE as a summary.
+ *
+ * TODO(353692849): add unit tests
+ */
+internal fun createInsightsIdeSummary(rawInsights: List<List<SlowStartReason>>): List<Insight> {
+ fun createInsightString(
+ criterion: SlowStartReason,
+ observed: List<IndexedValue<SlowStartReason>>
+ ): Insight {
+ observed.forEach {
+ require(it.value.reason_id == criterion.reason_id)
+ require(it.value.expected_value == criterion.expected_value)
+ }
+
+ val expectedValue = requireNotNull(criterion.expected_value)
+ val thresholdUnit = requireNotNull(expectedValue.unit)
+ require(thresholdUnit != ThresholdUnit.THRESHOLD_UNIT_UNSPECIFIED)
+ val unitSuffix =
+ when (thresholdUnit) {
+ ThresholdUnit.NS -> "ns"
+ ThresholdUnit.PERCENTAGE -> "%"
+ ThresholdUnit.COUNT -> " count"
+ ThresholdUnit.TRUE_OR_FALSE -> ""
+ else -> " ${thresholdUnit.toString().lowercase()}"
+ }
+
+ val criterionString = buildString {
+ append(requireNotNull(criterion.reason))
+ val thresholdValue = requireNotNull(expectedValue.value_)
+ append(" (expected: ")
+ if (thresholdUnit == ThresholdUnit.TRUE_OR_FALSE) {
+ require(thresholdValue in 0L..1L)
+ if (thresholdValue == 0L) append("false")
+ if (thresholdValue == 1L) append("true")
+ } else {
+ if (expectedValue.higher_expected == true) append("> ")
+ if (expectedValue.higher_expected == false) append("< ")
+ append(thresholdValue)
+ append(unitSuffix)
+ }
+ append(")")
+ }
+
+ val observedString =
+ observed.joinToString(" ", "seen in iterations: ") {
+ val actualValue = requireNotNull(it.value.actual_value?.value_)
+ val actualString: String =
+ if (thresholdUnit == ThresholdUnit.TRUE_OR_FALSE) {
+ require(actualValue in 0L..1L)
+ if (actualValue == 0L) "false" else "true"
+ } else {
+ "$actualValue$unitSuffix"
+ }
+ "${it.index}($actualString)"
+ }
+
+ return Insight(criterionString, observedString)
+ }
+
+ // Pivot from List<iteration_id -> insight_list> to List<insight -> iteration_list>
+ // and convert to a format expected in Studio text output.
+ return rawInsights
+ .flatMapIndexed { iterationId, insights -> insights.map { IndexedValue(iterationId, it) } }
+ .groupBy { it.value.reason_id }
+ .values
+ .map { createInsightString(it.first().value, it) }
+}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 1bd63b6..2b83006 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -26,6 +26,8 @@
import androidx.benchmark.Arguments
import androidx.benchmark.ConfigurationError
import androidx.benchmark.DeviceInfo
+import androidx.benchmark.ExperimentalBenchmarkConfigApi
+import androidx.benchmark.ExperimentalConfig
import androidx.benchmark.InstrumentationResults
import androidx.benchmark.Profiler
import androidx.benchmark.ResultWriter
@@ -34,13 +36,12 @@
import androidx.benchmark.conditionalError
import androidx.benchmark.inMemoryTrace
import androidx.benchmark.json.BenchmarkData
-import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig
import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig.InitialProcessState
-import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assume.assumeFalse
+import perfetto.protos.AndroidStartupMetric
/** Get package ApplicationInfo, throw if not found. */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -197,7 +198,7 @@
*
* This function is a building block for public testing APIs
*/
-@ExperimentalPerfettoCaptureApi
+@ExperimentalBenchmarkConfigApi
private fun macrobenchmark(
uniqueName: String,
className: String,
@@ -208,7 +209,7 @@
iterations: Int,
launchWithClearTask: Boolean,
startupModeMetricHint: StartupMode?,
- perfettoConfig: PerfettoConfig?,
+ experimentalConfig: ExperimentalConfig?,
perfettoSdkConfig: PerfettoSdkConfig?,
setupBlock: MacrobenchmarkScope.() -> Unit,
measureBlock: MacrobenchmarkScope.() -> Unit
@@ -273,7 +274,7 @@
scope = scope,
profiler = null, // Don't profile when measuring
metrics = metrics,
- perfettoConfig = perfettoConfig,
+ experimentalConfig = experimentalConfig,
perfettoSdkConfig = perfettoSdkConfig,
setupBlock = setupBlock,
measureBlock = measureBlock
@@ -292,7 +293,7 @@
scope = scope,
profiler = MethodTracingProfiler(scope),
metrics = emptyList(), // Nothing to measure
- perfettoConfig = perfettoConfig,
+ experimentalConfig = experimentalConfig,
perfettoSdkConfig = perfettoSdkConfig,
setupBlock = setupBlock,
measureBlock = measureBlock
@@ -303,11 +304,13 @@
val tracePaths = mutableListOf<String>()
val profilerResults = mutableListOf<Profiler.ResultFile>()
val measurementsList = mutableListOf<List<Metric.Measurement>>()
+ val insightsList = mutableListOf<List<AndroidStartupMetric.SlowStartReason>>()
outputs.forEach {
tracePaths += it.tracePaths
profilerResults += it.profilerResults
measurementsList += it.measurements
+ insightsList += it.insights
}
// Merge measurements
@@ -327,6 +330,7 @@
warningMessage = warningMessage,
testName = uniqueName,
measurements = measurements,
+ insights = createInsightsIdeSummary(insightsList),
iterationTracePaths = tracePaths,
profilerResults = profilerResults
)
@@ -372,7 +376,7 @@
/** Run a macrobenchmark with the specified StartupMode */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@ExperimentalPerfettoCaptureApi
+@ExperimentalBenchmarkConfigApi
fun macrobenchmarkWithStartupMode(
uniqueName: String,
className: String,
@@ -381,7 +385,7 @@
metrics: List<Metric>,
compilationMode: CompilationMode,
iterations: Int,
- perfettoConfig: PerfettoConfig?,
+ experimentalConfig: ExperimentalConfig?,
startupMode: StartupMode?,
setupBlock: MacrobenchmarkScope.() -> Unit,
measureBlock: MacrobenchmarkScope.() -> Unit
@@ -407,7 +411,7 @@
compilationMode = compilationMode,
iterations = iterations,
startupModeMetricHint = startupMode,
- perfettoConfig = perfettoConfig,
+ experimentalConfig = experimentalConfig,
perfettoSdkConfig = perfettoSdkConfig,
setupBlock = {
if (startupMode == StartupMode.COLD) {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkPhase.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkPhase.kt
index 1d56ae9..5adca1f 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkPhase.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkPhase.kt
@@ -18,9 +18,10 @@
import android.os.Build
import android.util.Log
+import androidx.benchmark.ExperimentalBenchmarkConfigApi
+import androidx.benchmark.ExperimentalConfig
import androidx.benchmark.Profiler
import androidx.benchmark.inMemoryTrace
-import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
import androidx.benchmark.perfetto.PerfettoCapture
import androidx.benchmark.perfetto.PerfettoCaptureWrapper
import androidx.benchmark.perfetto.PerfettoConfig
@@ -30,6 +31,8 @@
import androidx.benchmark.perfetto.appendUiState
import androidx.tracing.trace
import java.io.File
+import perfetto.protos.AndroidStartupMetric
+import perfetto.protos.TraceMetrics
/** A Profiler being used during a Macro Benchmark Phase. */
internal interface PhaseProfiler {
@@ -61,11 +64,12 @@
/** A list of profiler results obtained during a Macrobenchmark Phase. */
val profilerResults: List<Profiler.ResultFile> = emptyList(),
/** The list of measurements obtained per-iteration from the Macrobenchmark Phase. */
- val measurements: List<List<Metric.Measurement>> = emptyList()
+ val measurements: List<List<Metric.Measurement>> = emptyList(),
+ val insights: List<List<AndroidStartupMetric.SlowStartReason>> = emptyList()
)
/** Run a Macrobenchmark Phase and collect the [PhaseResult]. */
-@ExperimentalPerfettoCaptureApi
+@ExperimentalBenchmarkConfigApi
internal fun PerfettoTraceProcessor.runPhase(
uniqueName: String,
packageName: String,
@@ -75,7 +79,7 @@
scope: MacrobenchmarkScope,
profiler: PhaseProfiler?,
metrics: List<Metric>,
- perfettoConfig: PerfettoConfig?,
+ experimentalConfig: ExperimentalConfig?,
perfettoSdkConfig: PerfettoCapture.PerfettoSdkConfig?,
setupBlock: MacrobenchmarkScope.() -> Unit,
measureBlock: MacrobenchmarkScope.() -> Unit
@@ -85,6 +89,7 @@
val perfettoCollector = PerfettoCaptureWrapper()
val tracePaths = mutableListOf<String>()
val measurements = mutableListOf<List<Metric.Measurement>>()
+ val insights = mutableListOf<List<AndroidStartupMetric.SlowStartReason>>()
val profilerResultFiles = mutableListOf<Profiler.ResultFile>()
try {
// Configure metrics in the Phase.
@@ -105,7 +110,7 @@
perfettoCollector.record(
fileLabel = scope.fileLabel,
config =
- perfettoConfig
+ experimentalConfig?.perfettoConfig
?: PerfettoConfig.Benchmark(
/**
* Prior to API 24, every package name was joined into a single
@@ -159,10 +164,22 @@
File(tracePath).apply { appendUiState(uiState) }
// Accumulate measurements
- measurements +=
- loadTrace(PerfettoTrace(tracePath)) {
- // Extracts the metrics using the perfetto trace processor
- inMemoryTrace("extract metrics") {
+ loadTrace(PerfettoTrace(tracePath)) {
+ // Extracts the insights using the perfetto trace processor
+ if (experimentalConfig?.startupInsightsConfig?.isEnabled == true) {
+ inMemoryTrace("extract insights") {
+ insights +=
+ TraceMetrics.ADAPTER.decode(
+ queryMetricsProtoBinary(listOf("android_startup"))
+ )
+ .android_startup
+ ?.startup
+ ?.flatMap { it.slow_start_reason_with_details } ?: emptyList()
+ }
+ }
+ // Extracts the metrics using the perfetto trace processor
+ inMemoryTrace("extract metrics") {
+ measurements +=
metrics
// capture list of Measurements
.map {
@@ -178,8 +195,8 @@
}
// merge together
.reduceOrNull() { sum, element -> sum.merge(element) } ?: emptyList()
- }
}
+ }
}
} finally {
scope.killProcess()
@@ -187,6 +204,7 @@
return PhaseResult(
tracePaths = tracePaths,
profilerResults = profilerResultFiles,
- measurements = measurements
+ measurements = measurements,
+ insights = insights
)
}
diff --git a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
index 8b2eb89..20b7c15 100644
--- a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
+++ b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
@@ -24,7 +24,7 @@
import com.android.build.gradle.TestedExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
-import org.gradle.api.UnknownTaskException
+import org.gradle.api.Task
import org.gradle.api.tasks.StopExecutionException
import org.gradle.api.tasks.TaskContainer
@@ -120,21 +120,15 @@
val adbPathProvider = componentsExtension.sdkComponents.adb.map { it.asFile.absolutePath }
- if (!project.rootProject.tasks.exists("lockClocks")) {
- project.rootProject.tasks.register("lockClocks", LockClocksTask::class.java).configure {
- it.adbPath.set(adbPathProvider)
- it.coresArg.set(
- project.providers
- .gradleProperty("androidx.benchmark.lockClocks.cores")
- .orElse("")
- )
- }
+ project.tasks.maybeRegister("lockClocks", LockClocksTask::class.java).configure {
+ it.adbPath.set(adbPathProvider)
+ it.coresArg.set(
+ project.providers.gradleProperty("androidx.benchmark.lockClocks.cores").orElse("")
+ )
}
- if (!project.rootProject.tasks.exists("unlockClocks")) {
- project.rootProject.tasks
- .register("unlockClocks", UnlockClocksTask::class.java)
- .configure { it.adbPath.set(adbPathProvider) }
+ project.tasks.maybeRegister("unlockClocks", UnlockClocksTask::class.java).configure {
+ it.adbPath.set(adbPathProvider)
}
val extensionVariants =
@@ -236,11 +230,10 @@
}
}
- private fun TaskContainer.exists(taskName: String) =
+ private fun <T : Task> TaskContainer.maybeRegister(taskName: String, type: Class<T>) =
try {
- named(taskName)
- true
- } catch (e: UnknownTaskException) {
- false
+ named(taskName, type)
+ } catch (e: Exception) {
+ register(taskName, type)
}
}
diff --git a/buildSrc/lint.xml b/buildSrc/lint.xml
index 1993387..78d7c83 100644
--- a/buildSrc/lint.xml
+++ b/buildSrc/lint.xml
@@ -42,6 +42,7 @@
<issue id="MissingTestSizeAnnotation" severity="fatal" />
<issue id="IgnoreClassLevelDetector" severity="fatal" />
<issue id="WithPluginClasspathUsage" severity="fatal" />
+ <issue id="AutoValueNullnessOverride" severity="fatal" />
<!-- Disable all lint checks on transformed classes by default. b/283812176 -->
<issue id="all">
<ignore path="**/.transforms/**" />
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 2bd1ab6..ae6e4af 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -36,7 +36,6 @@
import com.android.build.api.dsl.TestExtension
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
-import com.android.build.api.variant.HasAndroidTest
import com.android.build.api.variant.HasDeviceTests
import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
@@ -564,6 +563,7 @@
private fun Project.getTestSourceSetsForAndroid(variant: Variant?): List<FileCollection> {
val testSourceFileCollections = mutableListOf<FileCollection>()
+ @Suppress("DEPRECATION") // usage of HasAndroidTest
when (variant) {
is TestVariant -> {
// com.android.test modules keep test code in main sourceset
@@ -579,7 +579,7 @@
// Note, don't have to add kotlin-multiplatform as it is not compatible with
// com.android.test modules
}
- is HasAndroidTest -> {
+ is com.android.build.api.variant.HasAndroidTest -> {
variant.androidTest?.sources?.java?.all?.let {
testSourceFileCollections.add(files(it))
}
diff --git a/busytown/androidx_host_tests_docker_2004.sh b/busytown/androidx_host_tests_docker_2004.sh
index 6856127..4c0b17b 100755
--- a/busytown/androidx_host_tests_docker_2004.sh
+++ b/busytown/androidx_host_tests_docker_2004.sh
@@ -5,4 +5,9 @@
cd "$(dirname $0)"
+impl/build.sh test allHostTests zipOwnersFiles createModuleInfo \
+ -Pandroidx.ignoreTestFailures \
+ -Pandroidx.displayTestOutput=false \
+ "$@"
+
echo "Completing $0 at $(date)"
diff --git a/busytown/impl/build.sh b/busytown/impl/build.sh
index 2a624ad..5f1ede1 100755
--- a/busytown/impl/build.sh
+++ b/busytown/impl/build.sh
@@ -66,6 +66,9 @@
# export some variables
ANDROID_HOME=../../prebuilts/fullsdk-linux
+export PATH="$ANDROID_HOME/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/python3/bin:$PATH"
+# Remove when b/366010045 is resolved: android platform build requires either en_US.UTF-8 or C.UTF-8 to exist
+export LC_ALL=C.UTF-8
BUILD_STATUS=0
# enable remote build cache unless explicitly disabled
@@ -77,15 +80,23 @@
# If our existing native libraries are newer, then we don't downgrade them because
# something else (like Bash) might be requiring the newer version.
function areNativeLibsNewEnoughForKonan() {
- host=`uname`
- if [[ "$host" == Darwin* ]]; then
+ if [[ "$(uname)" == Darwin* ]]; then
# we don't have any Macs having native dependencies too old to build KMP/konan
true
+ elif [[ -f /etc/os-release ]]; then
+ . /etc/os-release
+ version=${VERSION_ID//./} # Remove dots for comparison
+ if (( version >= 2004 )); then
+ true
+ else
+ # on Ubuntu < 20.04 we check whether we have a sufficiently new GLIBCXX
+ gcc --print-file-name=libstdc++.so.6 | xargs readelf -a -W | grep GLIBCXX_3.4.21 >/dev/null
+ fi
else
- # on Linux we check whether we have a sufficiently new GLIBCXX
- gcc --print-file-name=libstdc++.so.6 | xargs readelf -a -W | grep GLIBCXX_3.4.21 >/dev/null
+ true
fi
}
+
if ! areNativeLibsNewEnoughForKonan; then
KONAN_HOST_LIBS="$OUT_DIR/konan-host-libs"
LOG="$KONAN_HOST_LIBS.log"
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
index 0c41ed2..23fbcca 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
@@ -303,19 +303,22 @@
session?.disconnect()
camera?.disconnect()
}
- if (graphConfig.flags.closeCaptureSessionOnDisconnect) {
+ if (
+ graphConfig.flags.abortCapturesOnStop ||
+ graphConfig.flags.closeCaptureSessionOnDisconnect
+ ) {
// It seems that on certain devices, CameraCaptureSession.close() can block for an
// extended period of time [1]. Wrap the await call with a timeout to prevent us from
// getting blocked for too long.
//
// [1] b/307594946 - [ANR] at Camera2CameraController.disconnectSessionAndCamera
- runBlockingWithTimeout(threads.backgroundDispatcher, CLOSE_CAPTURE_SESSION_TIMEOUT_MS) {
+ runBlockingWithTimeout(threads.backgroundDispatcher, DISCONNECT_TIMEOUT_MS) {
deferred.await()
}
}
}
companion object {
- private const val CLOSE_CAPTURE_SESSION_TIMEOUT_MS = 2_000L // 2s
+ private const val DISCONNECT_TIMEOUT_MS = 2_000L // 2s
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CompositionSettings.java b/camera/camera-core/src/main/java/androidx/camera/core/CompositionSettings.java
index 9b44add..84241e4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CompositionSettings.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CompositionSettings.java
@@ -41,7 +41,22 @@
* cases.
*
* <p>The following code snippet demonstrates how to display in Picture-in-Picture mode:
- * <pre>{@code
+ * <pre>
+ * 16
+ * --------------------------------
+ * | c0 |
+ * | |
+ * | |
+ * | |
+ * | --------- | 9
+ * | | | |
+ * | | c1 | |
+ * | | | |
+ * | --------- |
+ * --------------------------------
+ * c0: primary camera
+ * c1: secondary camera
+ * {@code
* ResolutionSelector resolutionSelector = new ResolutionSelector.Builder()
* .setAspectRatioStrategy(
* AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
@@ -73,10 +88,7 @@
* .build(),
* lifecycleOwner);
* cameraProvider.bindToLifecycle(ImmutableList.of(primary, secondary));
- * }}</pre>
- *
- * <img src="/images/reference/androidx/camera/camera-core/
- * concurrent_camera_composition_settings.png"/>
+ * }</pre>
*/
public class CompositionSettings {
diff --git a/camera/camera-media3-effect/build.gradle b/camera/camera-media3-effect/build.gradle
index 4383949..e1fa31d 100644
--- a/camera/camera-media3-effect/build.gradle
+++ b/camera/camera-media3-effect/build.gradle
@@ -23,8 +23,8 @@
dependencies {
api(project(":camera:camera-core"))
- implementation(libs.media3Common)
- implementation(libs.media3Effect)
+ implementation("androidx.media3:media3-common:1.5.0-alpha01")
+ implementation("androidx.media3:media3-effect:1.5.0-alpha01")
testImplementation(libs.kotlinCoroutinesAndroid)
testImplementation(libs.kotlinCoroutinesTest)
@@ -42,8 +42,10 @@
androidTestImplementation(libs.truth)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.kotlinCoroutinesAndroid)
+ androidTestImplementation(project(":camera:camera-testing"))
}
android {
+ compileSdk 35
testOptions.unitTests.includeAndroidResources = true
namespace "androidx.camera.media3.effect"
}
diff --git a/camera/camera-media3-effect/src/androidTest/java/androidx/camera/media3/effect/Media3EffectDeviceTest.kt b/camera/camera-media3-effect/src/androidTest/java/androidx/camera/media3/effect/Media3EffectDeviceTest.kt
index 4840bb7..f78cc67 100644
--- a/camera/camera-media3-effect/src/androidTest/java/androidx/camera/media3/effect/Media3EffectDeviceTest.kt
+++ b/camera/camera-media3-effect/src/androidTest/java/androidx/camera/media3/effect/Media3EffectDeviceTest.kt
@@ -16,6 +16,13 @@
package androidx.camera.media3.effect
+import android.util.Size
+import androidx.camera.core.CameraEffect
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.utils.Threads.runOnMainSync
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -30,7 +37,56 @@
class Media3EffectDeviceTest {
@Test
- fun smokeTest() {
- assertThat(true).isTrue()
+ fun closeAClosedEffect_throwsException() {
+ // Arrange.
+ val media3Effect =
+ Media3Effect(
+ context = ApplicationProvider.getApplicationContext(),
+ targets = CameraEffect.PREVIEW,
+ executor = mainThreadExecutor(),
+ errorListener = { throw it }
+ )
+ var exception: Exception? = null
+
+ // Act: close the effect twice.
+ runOnMainSync {
+ media3Effect.close()
+ try {
+ media3Effect.close()
+ } catch (e: IllegalStateException) {
+ exception = e
+ }
+ }
+
+ // Assert: IllegalStateException was thrown.
+ assertThat(exception!!).isInstanceOf(IllegalStateException::class.java)
+ }
+
+ @Test
+ fun closeEffect_pendingRequestIsCancelled() {
+ // Arrange: create a Media3Effect and a SurfaceRequest.
+ val media3Effect =
+ Media3Effect(
+ context = ApplicationProvider.getApplicationContext(),
+ targets = CameraEffect.PREVIEW,
+ executor = mainThreadExecutor(),
+ errorListener = { throw it }
+ )
+ val surfaceRequest = SurfaceRequest(Size(10, 10), FakeCamera()) {}
+
+ // Act: provide the surface request and close the effect.
+ runOnMainSync {
+ media3Effect.surfaceProcessor!!.onInputSurface(surfaceRequest)
+ media3Effect.close()
+ }
+
+ // Assert: the surface request is cancelled.
+ var exception: Exception? = null
+ try {
+ surfaceRequest.deferrableSurface.surface.get()
+ } catch (e: Exception) {
+ exception = e
+ }
+ assertThat(exception!!.message).contains("Surface request will not complete.")
}
}
diff --git a/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/CameraXGlTransformation.kt b/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/CameraXGlTransformation.kt
new file mode 100644
index 0000000..7d2cd829
--- /dev/null
+++ b/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/CameraXGlTransformation.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.camera.media3.effect
+
+import android.annotation.SuppressLint
+import android.opengl.Matrix
+import androidx.media3.common.util.Size
+import androidx.media3.effect.GlMatrixTransformation
+
+/**
+ * A [GlMatrixTransformation] that samples the texture according to the orientation matrix and draws
+ * onto the new texture.
+ */
+@SuppressLint("UnsafeOptInUsageError")
+internal class CameraXGlTransformation(
+ cameraXTransformation: FloatArray,
+ private val outputSize: Size
+) : GlMatrixTransformation {
+ // Adjusted orientation matrix for vertex shader usage
+ private val glMatrix = FloatArray(16)
+
+ // Raw transformation matrix passed from SurfaceOutput
+ private val cameraXTransform = cameraXTransformation.clone()
+
+ /**
+ * Calculates the transformation needed by the media3 effect.
+ *
+ * {@inheritDoc}
+ *
+ * <gl_pos, f(A * B * NDC * gl_pos)> is how the GL shaders map the texture coordinates to NDC
+ * coordinates, where:
+ * - gl_pos: vertex position.
+ * - A: transformation provided by camera framework, representing the camera sensor orientation.
+ * - B: transformation provided by CameraX, representing device orientation and crop rect.
+ * - NDC: transformation from texture coordinate system to NDC coordinate system.
+ * - f() = color function given a position
+ *
+ * We desire a vertex and fragment pair that is scaled with: <gl_pos, f(A * B * NDC * gl_pos)>
+ * But the media3 effect shader does not allow the additional transformation B. Thus, by
+ * replacing gl_pos with NDC_inv * B_inv * NDC * gl_pos, we achieve:
+ *
+ * <NDC_inv * B_inv * NDC * gl_pos, f(A * NDC * gl_pos)>
+ *
+ * This method computes the glMatrix field to be equal to NDC_inv * B_inv * NDC. The shaders
+ * used by media3 effect are located in the following directory:
+ * https://github.com/androidx/media/tree/release/libraries/effect/src/main/assets/shaders
+ */
+ override fun configure(inputWidth: Int, inputHeight: Int): Size {
+ // glMatrix = NDC * B
+ Matrix.multiplyMM(glMatrix, 0, cameraXTransform, 0, NDC, 0)
+ // glMatrix = NDC * B * NDC_inv
+ Matrix.multiplyMM(glMatrix, 0, NDC_INV, 0, glMatrix, 0)
+ // glMatrix = (NDC * B * NDC_inv) ^ -1 = NDC_inv * B_inv * NDC
+ Matrix.invertM(glMatrix, 0, glMatrix, 0)
+ return outputSize
+ }
+
+ override fun getGlMatrixArray(presentationTimeUs: Long): FloatArray {
+ return glMatrix
+ }
+
+ private companion object {
+ // Matrix that maps texture space [0, 1] to NDC [-1, 1]
+ private val NDC =
+ floatArrayOf(
+ 0.5f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.5f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ 0.0f,
+ 0.5f,
+ 0.5f,
+ 0.0f,
+ 1.0f
+ )
+
+ // Matrix that maps NDC [-1, 1] to texture space [0, 1]
+ private val NDC_INV =
+ floatArrayOf(
+ 2.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 2.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ 0.0f,
+ -1.0f,
+ -1.0f,
+ 0.0f,
+ 1.0f
+ )
+ }
+}
diff --git a/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3Effect.kt b/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3Effect.kt
index 7ecd11a..3ea897d 100644
--- a/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3Effect.kt
+++ b/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3Effect.kt
@@ -16,17 +16,75 @@
package androidx.camera.media3.effect
+import android.annotation.SuppressLint
+import android.content.Context
import androidx.annotation.RestrictTo
import androidx.camera.core.CameraEffect
-import androidx.camera.core.SurfaceProcessor
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.core.util.Consumer
+import androidx.media3.common.Effect
import java.util.concurrent.Executor
-/** A CameraEffect that inserts media3 effect into CameraX pipeline */
+/**
+ * A CameraEffect that applies media3 [Effect] to the CameraX pipeline
+ *
+ * This class is an adapter between the CameraX [CameraEffect] API and the media3 [Effect] API. It
+ * allows the media3 [Effect] to be applied to the CameraX pipeline on the fly without restarting
+ * the camera.
+ *
+ * @param context the Android context
+ * @param targets the target UseCase to which this effect should be applied. For details, see
+ * [CameraEffect].
+ * @param executor the [Executor] on which the errorListener will be invoked.
+ * @param errorListener invoked if the effect runs into unrecoverable errors. The [Throwable] will
+ * be the error thrown by this [Media3Effect]. This is invoked on the provided executor.
+ */
+@SuppressLint("UnsafeOptInUsageError")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class Media3Effect(
+ context: Context,
targets: Int,
executor: Executor,
- surfaceProcessor: SurfaceProcessor,
- errorListener: Consumer<Throwable>
-) : CameraEffect(targets, executor, surfaceProcessor, errorListener) {}
+ errorListener: Consumer<Throwable>,
+) :
+ CameraEffect(
+ targets,
+ mainThreadExecutor(),
+ Media3SurfaceProcessor(
+ context,
+ executor,
+ errorListener,
+ ),
+ {}
+ ),
+ AutoCloseable {
+
+ /**
+ * Closes the [Media3Effect] and releases all the resources.
+ *
+ * <p>The caller should only close when the effect when it's no longer in use. Once closed, the
+ * effect should not be used again.
+ */
+ public override fun close() {
+ val glSurfaceProcessor = surfaceProcessor
+ if (glSurfaceProcessor is Media3SurfaceProcessor) {
+ glSurfaceProcessor.release()
+ }
+ }
+
+ /**
+ * Applies a list of media3 effects to the camera output.
+ *
+ * <p>Once set, the effects will be effective immediately. To clear the effect, call this method
+ * again with a empty list.
+ */
+ @SuppressLint("UnsafeOptInUsageError")
+ public fun setEffects(effects: List<Effect>) {
+ val glSurfaceProcessor = surfaceProcessor
+ if (glSurfaceProcessor is Media3SurfaceProcessor) {
+ glSurfaceProcessor.setEffects(effects)
+ }
+ }
+}
+
+internal const val TAG: String = "Media3Effect"
diff --git a/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3SurfaceProcessor.kt b/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3SurfaceProcessor.kt
new file mode 100644
index 0000000..9ada92a
--- /dev/null
+++ b/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3SurfaceProcessor.kt
@@ -0,0 +1,283 @@
+/*
+ * 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.camera.media3.effect
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.opengl.Matrix
+import androidx.annotation.MainThread
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceProcessor
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.utils.Threads.checkMainThread
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+import androidx.core.util.Consumer
+import androidx.media3.common.C
+import androidx.media3.common.ColorInfo
+import androidx.media3.common.DebugViewProvider
+import androidx.media3.common.Effect
+import androidx.media3.common.FrameInfo
+import androidx.media3.common.SurfaceInfo
+import androidx.media3.common.VideoFrameProcessingException
+import androidx.media3.common.VideoFrameProcessor
+import androidx.media3.common.util.Assertions.checkState
+import androidx.media3.common.util.Log
+import androidx.media3.common.util.Size
+import androidx.media3.effect.DefaultVideoFrameProcessor
+import java.util.concurrent.Executor
+
+/**
+ * A [SurfaceProcessor] that wraps around media3's [DefaultVideoFrameProcessor].
+ *
+ * CameraX’s CameraEffect API handles real-time changes of the input and output buffer, such as
+ * resolution, transformation, image format, FPS and/or dynamic range. On the other hand, media3’s
+ * DefaultVideoFrameProcessor API must configure the processor prior to its creation and stay
+ * unchanged during its lifetime. The adapter layer bridges this gap by listening to the CameraX
+ * callbacks [SurfaceProcessor.onOutputSurface] and [SurfaceProcessor.onInputSurface] for
+ * configuration changes, and destroys/creates [DefaultVideoFrameProcessor] as needed.
+ *
+ * Waiting for CameraX callbacks
+ * ^ | ^ ^
+ * | V yes | |
+ * | Connected? --> Disconnect -- |
+ * | | no |
+ * | V yes |
+ * | Ready to connect? --> Connect ----
+ * | | no
+ * --------------
+ */
+@SuppressLint("UnsafeOptInUsageError")
+internal class Media3SurfaceProcessor(
+ private val context: Context,
+ private val listenerExecutor: Executor,
+ private val errorListener: Consumer<Throwable>,
+) : SurfaceProcessor {
+ private var effects = emptyList<Effect>()
+
+ // The input and output that are pending to be connected.
+ private var pendingInput: SurfaceRequest? = null
+ private var pendingOutput: SurfaceOutput? = null
+
+ // The input, output and the processor that are currently connected.
+ private var connectedInput: SurfaceRequest? = null
+ private var connectedOutput: SurfaceOutput? = null
+ private var connectedProcessor: DefaultVideoFrameProcessor? = null
+
+ // The active processors. A processor remains active until the input/output are closed.
+ private val activeProcessors: MutableSet<DefaultVideoFrameProcessor> = HashSet()
+ private var isReleased = false
+
+ @MainThread
+ override fun onInputSurface(request: SurfaceRequest) {
+ checkMainThread()
+ Log.d(TAG, "onInputSurface $request")
+ if (isReleased) {
+ Log.w(TAG, "onInputSurface() called after release.")
+ return
+ }
+ pendingInput?.willNotProvideSurface()
+ pendingInput = request
+ disconnectProcessor(connectedProcessor)
+ tryConnectProcessor()
+ }
+
+ @MainThread
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+ checkMainThread()
+ Log.d(TAG, "onOutputSurface $surfaceOutput")
+ if (isReleased) {
+ Log.w(TAG, "onOutputSurface() called after release.")
+ return
+ }
+ connectedInput?.invalidate()
+ pendingOutput = surfaceOutput
+ disconnectProcessor(connectedProcessor)
+ tryConnectProcessor()
+ }
+
+ /** Internal API called by [Media3Effect] to update the current set of effects. */
+ @MainThread
+ fun setEffects(effects: List<Effect>) {
+ checkMainThread()
+ this.effects = effects.toList()
+ if (connectedInput != null || connectedOutput != null || connectedProcessor != null) {
+ configureProcessor(connectedInput!!, connectedOutput!!, connectedProcessor!!)
+ }
+ }
+
+ /** Internal API called by [Media3Effect] to release the processor. */
+ @MainThread
+ fun release() {
+ Log.d(TAG, "Release")
+ checkMainThread()
+ checkState(!isReleased, "The Media3Effect has already been released.")
+ isReleased = true
+ pendingInput?.willNotProvideSurface()
+ disconnectProcessor(connectedProcessor)
+ }
+
+ @MainThread
+ private fun disconnectProcessor(processor: DefaultVideoFrameProcessor?) {
+ checkMainThread()
+ Log.d(TAG, "disconnectProcessor: $processor")
+ if (processor != null && activeProcessors.contains(processor)) {
+ activeProcessors.remove(processor)
+ processor.release()
+ }
+ }
+
+ @MainThread
+ private fun tryConnectProcessor() {
+ checkMainThread()
+ Log.d(TAG, "tryConnectPendingProcessor")
+ val input = pendingInput
+ // SurfaceOutput can be recycled. It's OK to used a connected output.
+ val output = pendingOutput ?: connectedOutput
+ if (input != null && output != null) {
+ connectInputAndOutput(input, output)
+ }
+ }
+
+ /** Creates a media3 [ColorInfo] based on the CameraX [DynamicRange]. */
+ @MainThread
+ private fun createColorInfo(dynamicRange: DynamicRange): ColorInfo {
+ if (dynamicRange.is10BitHdr) {
+ val builder =
+ ColorInfo.Builder()
+ .setColorRange(C.COLOR_RANGE_LIMITED)
+ .setColorSpace(C.COLOR_SPACE_BT2020)
+ if (dynamicRange.encoding == DynamicRange.ENCODING_HLG) {
+ builder.setColorTransfer(C.COLOR_TRANSFER_HLG)
+ } else {
+ builder.setColorTransfer(C.COLOR_TRANSFER_ST2084)
+ }
+ return builder.build()
+ } else {
+ return ColorInfo.Builder()
+ .setColorSpace(C.COLOR_SPACE_BT601)
+ .setColorRange(C.COLOR_RANGE_FULL)
+ .setColorTransfer(C.COLOR_TRANSFER_SDR)
+ .build()
+ }
+ }
+
+ /**
+ * Configures the [processor] based on CameraX's input config, output config and the current set
+ * of media3 effects.
+ */
+ @MainThread
+ private fun configureProcessor(
+ input: SurfaceRequest,
+ output: SurfaceOutput,
+ processor: DefaultVideoFrameProcessor
+ ) {
+ // Gets user configured transformation from CameraX Effects API, and build a media3 effect
+ // that applies that transformation.
+ val identityMatrix = FloatArray(16)
+ Matrix.setIdentityM(identityMatrix, 0)
+ val cameraXTransform = FloatArray(16)
+ output.updateTransformMatrix(cameraXTransform, identityMatrix)
+ val cameraXTransformEffect =
+ CameraXGlTransformation(cameraXTransform, Size(output.size.width, output.size.height))
+ // Configure the processor's input format
+ val frameInfo =
+ FrameInfo.Builder(
+ createColorInfo(input.dynamicRange),
+ input.resolution.width,
+ input.resolution.height
+ )
+ .build()
+ processor.registerInputStream(
+ VideoFrameProcessor.INPUT_TYPE_SURFACE_AUTOMATIC_FRAME_REGISTRATION,
+ listOf(cameraXTransformEffect, *effects.toTypedArray()),
+ frameInfo
+ )
+ }
+
+ @MainThread
+ private fun connectInputAndOutput(input: SurfaceRequest, output: SurfaceOutput) {
+ checkMainThread()
+ Log.d(TAG, "connectInputAndOutput input: $input output: $output")
+ val wrappingListener =
+ object : VideoFrameProcessor.Listener {
+ override fun onInputStreamRegistered(
+ inputType: Int,
+ effects: MutableList<Effect>,
+ frameInfo: FrameInfo
+ ) {}
+
+ override fun onOutputSizeChanged(width: Int, height: Int) {}
+
+ override fun onOutputFrameAvailableForRendering(presentationTimeUs: Long) {}
+
+ override fun onError(exception: VideoFrameProcessingException) {
+ listenerExecutor.execute { errorListener.accept(exception) }
+ }
+
+ override fun onEnded() {}
+ }
+ val newProcessor =
+ DefaultVideoFrameProcessor.Factory.Builder()
+ .build()
+ .create(
+ context,
+ DebugViewProvider.NONE,
+ createColorInfo(input.dynamicRange),
+ /*renderFramesAutomatically=*/ true,
+ directExecutor(),
+ wrappingListener
+ )
+ Log.d(TAG, "Created processor $newProcessor")
+ configureProcessor(input, output, newProcessor)
+ activeProcessors.add(newProcessor)
+ // Prove input service when ready
+ newProcessor.setOnInputSurfaceReadyListener {
+ input.provideSurface(newProcessor.inputSurface, mainThreadExecutor()) {
+ Log.d(TAG, "Input surface life ends $input")
+ disconnectProcessor(newProcessor)
+ if (connectedInput == input) {
+ connectedInput = null
+ }
+ }
+ }
+ connectedInput = input
+ // Bind the output surface to frame processor
+ val outputSurface =
+ output.getSurface(mainThreadExecutor()) {
+ Log.d(TAG, "Output surface life ends $output")
+ disconnectProcessor(newProcessor)
+ output.close()
+ if (connectedOutput == output) {
+ connectedOutput = null
+ }
+ }
+ newProcessor.setOutputSurfaceInfo(
+ SurfaceInfo(
+ outputSurface,
+ output.size.width,
+ output.size.height,
+ /* orientationDegrees= */ 0
+ )
+ )
+ connectedOutput = output
+ // Pending values have been used
+ pendingInput = null
+ pendingOutput = null
+ connectedProcessor = newProcessor
+ }
+}
diff --git a/camera/integration-tests/viewtestapp/build.gradle b/camera/integration-tests/viewtestapp/build.gradle
index 6a72f25..e4e93c6 100644
--- a/camera/integration-tests/viewtestapp/build.gradle
+++ b/camera/integration-tests/viewtestapp/build.gradle
@@ -22,6 +22,7 @@
}
android {
+ compileSdk 35
defaultConfig {
applicationId "androidx.camera.integration.view"
}
diff --git a/car/app/app/api/1.7.0-beta02.txt b/car/app/app/api/1.7.0-beta02.txt
index 84a2ab4..966b18f 100644
--- a/car/app/app/api/1.7.0-beta02.txt
+++ b/car/app/app/api/1.7.0-beta02.txt
@@ -918,6 +918,7 @@
field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
field public static final String KEY_DESCRIPTION_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+ field public static final String KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST = "androidx.car.app.mediaextensions.KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST";
field public static final String KEY_IMMERSIVE_AUDIO = "androidx.car.app.mediaextensions.KEY_IMMERSIVE_AUDIO";
field public static final String KEY_SUBTITLE_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
}
diff --git a/car/app/app/api/current.ignore b/car/app/app/api/current.ignore
index e1d8307..0180f80 100644
--- a/car/app/app/api/current.ignore
+++ b/car/app/app/api/current.ignore
@@ -1,3 +1,3 @@
// Baseline format: 1.0
-AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
- Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
+AddedField: androidx.car.app.mediaextensions.MetadataExtras#KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST:
+ Added field androidx.car.app.mediaextensions.MetadataExtras.KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 84a2ab4..966b18f 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -918,6 +918,7 @@
field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
field public static final String KEY_DESCRIPTION_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+ field public static final String KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST = "androidx.car.app.mediaextensions.KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST";
field public static final String KEY_IMMERSIVE_AUDIO = "androidx.car.app.mediaextensions.KEY_IMMERSIVE_AUDIO";
field public static final String KEY_SUBTITLE_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
}
diff --git a/car/app/app/api/restricted_1.7.0-beta02.txt b/car/app/app/api/restricted_1.7.0-beta02.txt
index 84a2ab4..966b18f 100644
--- a/car/app/app/api/restricted_1.7.0-beta02.txt
+++ b/car/app/app/api/restricted_1.7.0-beta02.txt
@@ -918,6 +918,7 @@
field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
field public static final String KEY_DESCRIPTION_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+ field public static final String KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST = "androidx.car.app.mediaextensions.KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST";
field public static final String KEY_IMMERSIVE_AUDIO = "androidx.car.app.mediaextensions.KEY_IMMERSIVE_AUDIO";
field public static final String KEY_SUBTITLE_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
}
diff --git a/car/app/app/api/restricted_current.ignore b/car/app/app/api/restricted_current.ignore
index e1d8307..0180f80 100644
--- a/car/app/app/api/restricted_current.ignore
+++ b/car/app/app/api/restricted_current.ignore
@@ -1,3 +1,3 @@
// Baseline format: 1.0
-AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
- Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
+AddedField: androidx.car.app.mediaextensions.MetadataExtras#KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST:
+ Added field androidx.car.app.mediaextensions.MetadataExtras.KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 84a2ab4..966b18f 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -918,6 +918,7 @@
field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
field public static final String KEY_DESCRIPTION_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+ field public static final String KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST = "androidx.car.app.mediaextensions.KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST";
field public static final String KEY_IMMERSIVE_AUDIO = "androidx.car.app.mediaextensions.KEY_IMMERSIVE_AUDIO";
field public static final String KEY_SUBTITLE_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
}
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MetadataExtras.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MetadataExtras.java
index 6ab7697..0cb3fc4 100644
--- a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MetadataExtras.java
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MetadataExtras.java
@@ -106,6 +106,16 @@
"androidx.car.app.mediaextensions.KEY_IMMERSIVE_AUDIO";
/**
+ * {@link Bundle} key used in the extras of a media item to indicate that the metadata of this
+ * media item should not be shown next to content from other applications
+ *
+ * <p>TYPE: long - to enable, use value
+ * {@link androidx.media.utils.MediaConstants#METADATA_VALUE_ATTRIBUTE_PRESENT}</p>
+ */
+ public static final String KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST =
+ "androidx.car.app.mediaextensions.KEY_EXCLUDE_MEDIA_ITEM_FROM_MIXED_APP_LIST";
+
+ /**
* {@link Bundle} key used in the extras of a media item to indicate a tintable vector drawable
* representing its content format. This drawable must be rendered in large views showing
* information about the currently playing media item, in an area roughly equivalent to 15
diff --git a/compose/foundation/foundation-layout/api/current.txt b/compose/foundation/foundation-layout/api/current.txt
index 3b13e1e..45e5e44 100644
--- a/compose/foundation/foundation-layout/api/current.txt
+++ b/compose/foundation/foundation-layout/api/current.txt
@@ -372,6 +372,7 @@
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier consumeWindowInsets(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.PaddingValues paddingValues);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier consumeWindowInsets(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier onConsumedWindowInsetsChanged(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.WindowInsets,kotlin.Unit> block);
+ method public static androidx.compose.ui.Modifier recalculateWindowInsets(androidx.compose.ui.Modifier);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsPadding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
}
diff --git a/compose/foundation/foundation-layout/api/restricted_current.txt b/compose/foundation/foundation-layout/api/restricted_current.txt
index a4873d2..0d70ee8 100644
--- a/compose/foundation/foundation-layout/api/restricted_current.txt
+++ b/compose/foundation/foundation-layout/api/restricted_current.txt
@@ -380,6 +380,7 @@
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier consumeWindowInsets(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.PaddingValues paddingValues);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier consumeWindowInsets(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier onConsumedWindowInsetsChanged(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.WindowInsets,kotlin.Unit> block);
+ method public static androidx.compose.ui.Modifier recalculateWindowInsets(androidx.compose.ui.Modifier);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsPadding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
}
diff --git a/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsPaddingSample.kt b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsPaddingSample.kt
index 5c0c570..05fb0580 100644
--- a/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsPaddingSample.kt
+++ b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsPaddingSample.kt
@@ -22,6 +22,7 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.MutableWindowInsets
import androidx.compose.foundation.layout.PaddingValues
@@ -32,6 +33,7 @@
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.mandatorySystemGesturesPadding
@@ -39,6 +41,7 @@
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.recalculateWindowInsets
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.safeDrawingPadding
@@ -52,7 +55,14 @@
import androidx.compose.foundation.layout.waterfallPadding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
@@ -378,3 +388,71 @@
}
}
}
+
+@Sampled
+@Composable
+fun recalculateWindowInsetsSample() {
+ var hasFirstItem by remember { mutableStateOf(true) }
+ var hasLastItem by remember { mutableStateOf(true) }
+ Column(Modifier.fillMaxSize()) {
+ if (hasFirstItem) {
+ Box(Modifier.weight(1f).fillMaxWidth().background(Color.Magenta))
+ }
+ Box(
+ Modifier.fillMaxWidth() // force a fixed size on the content
+ .recalculateWindowInsets()
+ .weight(1f)
+ .background(Color.Yellow)
+ .safeDrawingPadding()
+ ) {
+ Button(
+ onClick = { hasFirstItem = !hasFirstItem },
+ Modifier.align(Alignment.TopCenter)
+ ) {
+ val action = if (hasFirstItem) "Remove" else "Add"
+ Text("$action First Item")
+ }
+ Button(
+ onClick = { hasLastItem = !hasLastItem },
+ Modifier.align(Alignment.BottomCenter)
+ ) {
+ val action = if (hasLastItem) "Remove" else "Add"
+ Text("$action Last Item")
+ }
+ }
+ if (hasLastItem) {
+ Box(Modifier.weight(1f).fillMaxWidth().background(Color.Cyan))
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun consumeWindowInsetsWithPaddingSample() {
+ // The outer Box uses padding and properly compensates for it by using consumeWindowInsets()
+ Box(
+ Modifier.fillMaxSize()
+ .padding(10.dp)
+ .consumeWindowInsets(WindowInsets(10.dp, 10.dp, 10.dp, 10.dp))
+ ) {
+ Box(Modifier.fillMaxSize().safeContentPadding().background(Color.Blue))
+ }
+}
+
+@Sampled
+@Composable
+fun unconsumedWindowInsetsWithPaddingSample() {
+ // This outer Box is representing a 3rd-party layout that you don't control. It has a
+ // padding, but doesn't properly use consumeWindowInsets()
+ Box(Modifier.padding(10.dp)) {
+ // This is the content that you control. You can make sure that the WindowInsets are correct
+ // so you can pad your content despite the fact that the parent did not
+ // consumeWindowInsets()
+ Box(
+ Modifier.fillMaxSize() // Force a fixed size on the content
+ .recalculateWindowInsets()
+ .safeContentPadding()
+ .background(Color.Blue)
+ )
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
index f53db0b..0dd2c5d 100644
--- a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
+++ b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
@@ -5765,7 +5765,7 @@
Layout(content) { measurables, constraints ->
val measurable = measurables.firstOrNull()
// The child cannot be larger than our max constraints, but we ignore min constraints.
- val placeable = measurable?.measure(constraints.copy(minWidth = 0, minHeight = 0))
+ val placeable = measurable?.measure(constraints.copyMaxDimensions())
// The layout is as large as possible for bounded constraints,
// or wrap content otherwise.
diff --git a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
index ffc24e7..a3a1bc5 100644
--- a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
+++ b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
@@ -30,14 +30,18 @@
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
+import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -47,6 +51,7 @@
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.graphics.Insets as AndroidXInsets
@@ -122,6 +127,158 @@
}
}
+ @Test
+ fun recalculateWindowInsets() {
+ var coordinates: LayoutCoordinates? = null
+
+ var padding by mutableIntStateOf(10)
+
+ setContent {
+ val paddingDp = with(LocalDensity.current) { padding.toDp() }
+ Box(Modifier.padding(paddingDp)) {
+ Box(Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding()) {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { coordinates = it })
+ }
+ }
+ }
+
+ rule.waitUntil { coordinates != null }
+ val coords = coordinates!!
+
+ sendInsets(WindowInsetsCompat.Type.systemBars(), AndroidXInsets.of(11, 17, 23, 29))
+
+ rule.waitUntil { // older devices animate the insets
+ rule.runOnUiThread { coords.boundsInRoot().top > 16.99f }
+ }
+
+ rule.runOnIdle {
+ val bounds = coords.boundsInRoot()
+ val rootSize = coords.findRootCoordinates().size
+ assertThat(bounds.left).isWithin(0.1f).of(11f)
+ assertThat(bounds.top).isWithin(0.1f).of(17f)
+ assertThat(bounds.right).isWithin(0.1f).of(rootSize.width - 23f)
+ assertThat(bounds.bottom).isWithin(0.1f).of(rootSize.height - 29f)
+
+ padding = 5
+ }
+
+ rule.runOnIdle {
+ val bounds = coords.boundsInRoot()
+ val rootSize = coords.findRootCoordinates().size
+ assertThat(bounds.left).isWithin(0.1f).of(11f)
+ assertThat(bounds.top).isWithin(0.1f).of(17f)
+ assertThat(bounds.right).isWithin(0.1f).of(rootSize.width - 23f)
+ assertThat(bounds.bottom).isWithin(0.1f).of(rootSize.height - 29f)
+
+ padding = 20
+ }
+
+ rule.runOnIdle {
+ val bounds = coords.boundsInRoot()
+ val rootSize = coords.findRootCoordinates().size
+ assertThat(bounds.left).isWithin(0.1f).of(20f)
+ assertThat(bounds.top).isWithin(0.1f).of(20f)
+ assertThat(bounds.right).isWithin(0.1f).of(rootSize.width - 23f)
+ assertThat(bounds.bottom).isWithin(0.1f).of(rootSize.height - 29f)
+ }
+ }
+
+ @Test
+ fun recalculateWindowInsetsWithMovement() {
+ var coordinates: LayoutCoordinates? = null
+
+ var alignment by mutableStateOf(AbsoluteAlignment.TopLeft)
+ setContent {
+ Box(Modifier.background(Color.Blue)) {
+ val sizeDp = with(LocalDensity.current) { 100.toDp() }
+ Box(
+ Modifier.size(sizeDp)
+ .recalculateWindowInsets()
+ .safeDrawingPadding()
+ .align(alignment)
+ .background(Color.Yellow)
+ .onPlaced { coordinates = it }
+ )
+ }
+ }
+
+ rule.waitUntil { coordinates != null }
+ val coords = coordinates!!
+
+ sendInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(11, 17, 23, 29))
+ rule.waitUntil { // older devices animate the insets
+ rule.runOnUiThread { coords.boundsInRoot().top > 16.9f }
+ }
+ rule.runOnIdle {
+ val bounds = coords.boundsInRoot()
+ assertThat(bounds.left).isWithin(0.1f).of(11f)
+ assertThat(bounds.top).isWithin(0.1f).of(17f)
+ assertThat(bounds.right).isWithin(0.1f).of(100f)
+ assertThat(bounds.bottom).isWithin(0.1f).of(100f)
+
+ alignment = AbsoluteAlignment.BottomRight
+ }
+
+ rule.runOnIdle {
+ val bounds = coords.boundsInRoot()
+ val rootSize = coords.findRootCoordinates().size
+ assertThat(bounds.left).isWithin(0.1f).of(rootSize.width - 100f)
+ assertThat(bounds.top).isWithin(0.1f).of(rootSize.height - 100f)
+ assertThat(bounds.right).isWithin(0.1f).of(rootSize.width - 23f)
+ assertThat(bounds.bottom).isWithin(0.1f).of(rootSize.height - 29f)
+ }
+ }
+
+ @Test
+ fun recalculateWindowInsetsWithNestedMovement() {
+ val coordinates = mutableStateOf<LayoutCoordinates?>(null)
+
+ var alignment by mutableStateOf(AbsoluteAlignment.TopLeft)
+ var size = 0f
+ setContent {
+ size = with(LocalDensity.current) { 100.dp.toPx() }
+ Box(Modifier.background(Color.Blue)) {
+ Box(Modifier.size(100.dp).align(alignment)) {
+ Box(Modifier.requiredSize(100.dp)) {
+ Box(
+ Modifier.size(100.dp)
+ .recalculateWindowInsets()
+ .safeDrawingPadding()
+ .background(Color.Yellow)
+ .onPlaced { coordinates.value = it }
+ )
+ }
+ }
+ }
+ }
+
+ rule.waitUntil { coordinates.value != null }
+ val coords = coordinates.value!!
+
+ sendInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(11, 17, 23, 29))
+ rule.waitUntil { // older devices animate the insets
+ rule.runOnUiThread { coords.boundsInRoot().top > 16.9f }
+ }
+ rule.runOnIdle {
+ val bounds = coords.boundsInRoot()
+ assertThat(bounds.left).isWithin(1f).of(11f)
+ assertThat(bounds.top).isWithin(1f).of(17f)
+ assertThat(bounds.right).isWithin(1f).of(size)
+ assertThat(bounds.bottom).isWithin(1f).of(size)
+
+ alignment = AbsoluteAlignment.BottomRight
+ }
+
+ rule.runOnIdle {
+ val bounds = coords.boundsInRoot()
+ val rootSize = coords.findRootCoordinates().size
+ assertThat(bounds.left).isWithin(1f).of(rootSize.width - size)
+ assertThat(bounds.top).isWithin(1f).of(rootSize.height - size)
+ assertThat(bounds.right).isWithin(1f).of(rootSize.width - 23f)
+ assertThat(bounds.bottom).isWithin(1f).of(rootSize.height - 29f)
+ }
+ }
+
private fun sendDisplayCutoutInsets(width: Int, height: Int): WindowInsetsCompat {
val centerWidth = width / 2
val centerHeight = height / 2
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt
index 73d7417..82f8b10 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt
@@ -133,7 +133,7 @@
if (propagateMinConstraints) {
constraints
} else {
- constraints.copy(minWidth = 0, minHeight = 0)
+ constraints.copyMaxDimensions()
}
if (measurables.size == 1) {
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
index 070aaa0..f916aad 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
@@ -23,20 +23,37 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.findRootCoordinates
+import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalMap
+import androidx.compose.ui.modifier.ModifierLocalModifierNode
import androidx.compose.ui.modifier.ModifierLocalProvider
import androidx.compose.ui.modifier.ModifierLocalReadScope
import androidx.compose.ui.modifier.ProvidableModifierLocal
+import androidx.compose.ui.modifier.modifierLocalMapOf
import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.node.GlobalPositionAwareModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.invalidatePlacement
+import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.offset
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.util.fastRoundToInt
/**
* Adds padding so that the content doesn't enter [insets] space.
@@ -319,6 +336,34 @@
*/
expect fun Modifier.mandatorySystemGesturesPadding(): Modifier
+/**
+ * This recalculates the [WindowInsets] based on the size and position. This only works when
+ * [Constraints] have [fixed width][Constraints.hasFixedWidth] and
+ * [fixed height][Constraints.hasFixedHeight]. This can be accomplished, for example, by having
+ * [Modifier.size], or [Modifier.fillMaxSize], or other size modifier before
+ * [recalculateWindowInsets]. If the [Constraints] sizes aren't fixed, [recalculateWindowInsets]
+ * won't adjust the [WindowInsets] and won't have any affect on layout.
+ *
+ * [recalculateWindowInsets] is useful when the parent does not call [consumeWindowInsets] when it
+ * aligns a child. For example, a [Column] with two children should have different [WindowInsets]
+ * for each child. The top item should exclude insets below its bottom and the bottom item should
+ * exclude the top insets, but the Column can't assign different insets for different children.
+ *
+ * @sample androidx.compose.foundation.layout.samples.recalculateWindowInsetsSample
+ *
+ * Another use is when a parent doesn't properly [consumeWindowInsets] for all space that it
+ * consumes. For example, a 3rd-party container has padding that doesn't properly use
+ * [consumeWindowInsets].
+ *
+ * @sample androidx.compose.foundation.layout.samples.unconsumedWindowInsetsWithPaddingSample
+ *
+ * In most cases you should not need to use this API, and the parent should instead use
+ * [consumeWindowInsets] to provide the correct values
+ *
+ * @sample androidx.compose.foundation.layout.samples.consumeWindowInsetsWithPaddingSample
+ */
+fun Modifier.recalculateWindowInsets(): Modifier = this.then(RecalculateWindowInsetsModifierElement)
+
internal val ModifierLocalConsumedWindowInsets = modifierLocalOf { WindowInsets(0, 0, 0, 0) }
internal class InsetsPaddingModifier(private val insets: WindowInsets) :
@@ -464,3 +509,114 @@
override fun hashCode(): Int = insets.hashCode()
}
+
+private object RecalculateWindowInsetsModifierElement :
+ ModifierNodeElement<RecalculateWindowInsetsModifierNode>() {
+ override fun create(): RecalculateWindowInsetsModifierNode =
+ RecalculateWindowInsetsModifierNode()
+
+ override fun hashCode(): Int = 0
+
+ override fun equals(other: Any?): Boolean = other === this
+
+ override fun update(node: RecalculateWindowInsetsModifierNode) {}
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "recalculateWindowInsets"
+ }
+}
+
+private class RecalculateWindowInsetsModifierNode :
+ Modifier.Node(),
+ ModifierLocalModifierNode,
+ LayoutModifierNode,
+ GlobalPositionAwareModifierNode {
+ val insets = ValueInsets(InsetsValues(0, 0, 0, 0), "reset")
+ var oldPosition = IntOffset.Zero
+
+ override val providedValues: ModifierLocalMap =
+ modifierLocalMapOf(ModifierLocalConsumedWindowInsets to insets)
+
+ override val shouldAutoInvalidate: Boolean
+ get() = false
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ return if (!constraints.hasFixedWidth || !constraints.hasFixedHeight) {
+ // We can't provide the modifier local value.
+ // We'll fall back to measuring the contents without providing the value
+ provide(ModifierLocalConsumedWindowInsets, ModifierLocalConsumedWindowInsets.current)
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+ } else {
+ val width = constraints.maxWidth
+ val height = constraints.maxHeight
+ layout(width, height) {
+ val coordinates = coordinates
+ coordinates?.let { oldPosition = it.positionInRoot().round() }
+ val windowInsets =
+ if (coordinates == null) {
+ // We don't know where we are, so can't reset the value. Use the old value.
+ ModifierLocalConsumedWindowInsets.current
+ } else {
+ val topLeft = coordinates.positionInRoot()
+ val size = coordinates.size
+ val bottomRight =
+ coordinates.localToRoot(
+ Offset(size.width.toFloat(), size.height.toFloat())
+ )
+ val root = coordinates.findRootCoordinates()
+ val rootSize = root.size
+ val left = topLeft.x.fastRoundToInt()
+ val top = topLeft.y.fastRoundToInt()
+ val right = rootSize.width - bottomRight.x.fastRoundToInt()
+ val bottom = rootSize.height - bottomRight.y.fastRoundToInt()
+ val oldValues = insets.value
+ if (
+ oldValues.left != left ||
+ oldValues.top != top ||
+ oldValues.right != right ||
+ oldValues.bottom != bottom
+ ) {
+ insets.value = InsetsValues(left, top, right, bottom)
+ }
+ insets
+ }
+ provide(ModifierLocalConsumedWindowInsets, windowInsets)
+ val placeable = measurable.measure(Constraints.fixed(width, height))
+ placeable.place(0, 0)
+ }
+ }
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int = measurable.minIntrinsicHeight(width)
+
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int = measurable.minIntrinsicWidth(height)
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int = measurable.maxIntrinsicHeight(width)
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int = measurable.maxIntrinsicWidth(height)
+
+ override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
+ val newPosition = coordinates.positionInRoot().round()
+ val hasMoved = oldPosition != newPosition
+ oldPosition = newPosition
+ if (hasMoved) {
+ invalidatePlacement()
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
index abeabd3..1132f6f 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
@@ -16,6 +16,9 @@
package androidx.compose.foundation.text.modifiers
+import androidx.compose.foundation.text.AutoSize
+import androidx.compose.foundation.text.DefaultMinLines
+import androidx.compose.foundation.text.FontSizeSearchScope
import androidx.compose.foundation.text.TEST_FONT_FAMILY
import androidx.compose.foundation.text.toIntPx
import androidx.compose.ui.text.AnnotatedString
@@ -28,6 +31,8 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -91,6 +96,7 @@
true,
Int.MAX_VALUE,
-1,
+ null,
null
)
val after = textDelegate.intrinsicHeight(20, LayoutDirection.Ltr)
@@ -307,6 +313,352 @@
}
@Test
+ fun TextLayoutResult_autoSize_oneSize_checkOverflowAndHeight() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text = AnnotatedString(text),
+ style = TextStyle(fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizePreset(arrayOf(25.6.sp))
+ )
+ .also { it.density = density }
+
+ // 25.6.sp doesn't overflow
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ var layoutResult = layoutCache.textLayoutResult
+ assertThat(layoutResult.hasVisualOverflow).isFalse()
+ assertThat(layoutResult.multiParagraph.height).isEqualTo(100)
+
+ layoutCache.updateAutoSize(
+ text = "Hello World",
+ fontSize = TextUnit.Unspecified,
+ autoSize = AutoSizePreset(arrayOf(25.7.sp))
+ )
+
+ // 25.7.sp does overflow
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ layoutResult = layoutCache.textLayoutResult
+ assertThat(layoutResult.hasVisualOverflow).isTrue()
+ assertThat(layoutResult.multiParagraph.height).isEqualTo(1000)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_multipleSizes_checkOverflowAndHeight() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text = AnnotatedString(text),
+ style = TextStyle(fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizePreset(arrayOf(23.5.sp, 22.sp, 25.6.sp))
+ )
+ .also { it.density = density }
+
+ // All font sizes shouldn't overflow
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ var layoutResult = layoutCache.textLayoutResult
+ assertThat(layoutResult.hasVisualOverflow).isFalse()
+ assertThat(layoutResult.multiParagraph.height).isEqualTo(100)
+
+ layoutCache.updateAutoSize(
+ text = text,
+ fontSize = TextUnit.Unspecified,
+ autoSize = AutoSizePreset(arrayOf(25.7.sp, 25.6.sp, 50.sp))
+ )
+
+ // Only 25.6.sp shouldn't overflow
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ layoutResult = layoutCache.textLayoutResult
+ assertThat(layoutResult.hasVisualOverflow).isFalse()
+ assertThat(layoutResult.multiParagraph.height).isEqualTo(100)
+
+ layoutCache.updateAutoSize(
+ text = text,
+ fontSize = TextUnit.Unspecified,
+ autoSize = AutoSizePreset(arrayOf(25.9.sp, 25.7.sp, 50.sp))
+ )
+
+ // All font sizes should overflow
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ layoutResult = layoutCache.textLayoutResult
+ assertThat(layoutResult.hasVisualOverflow).isTrue()
+ assertThat(layoutResult.multiParagraph.height).isEqualTo(1000)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_changeConstraints_doesOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 50, minHeight = 0, maxHeight = 50)
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text = AnnotatedString("Hello World"),
+ style = TextStyle(fontSize = 20.sp, fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSize.StepBased(20.sp, 51.sp, 1.sp)
+ )
+ .also { it.density = density }
+
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = layoutCache.textLayoutResult
+ // this should overflow - 20.sp is too large a font size to use for the smaller constraints
+ assertThat(layoutResult.hasVisualOverflow).isTrue()
+ assertThat(layoutResult.multiParagraph.height).isEqualTo(120)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_textLongerThan30Characters_doesOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text =
+ AnnotatedString(
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec egestas " +
+ "sollicitudin arcu, sed mattis orci gravida vel. Donec luctus turpis."
+ ),
+ style = TextStyle(fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSize.StepBased(20.sp, 51.sp, 1.sp)
+ )
+ .also { it.density = density }
+
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = layoutCache.textLayoutResult
+ // this should overflow - 20.sp is too large of a font size to use for the longer text
+ assertThat(layoutResult.hasVisualOverflow).isTrue()
+ assertThat(layoutResult.multiParagraph.height).isEqualTo(600)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_ellipsized_isLineEllipsized() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec egestas " +
+ "sollicitudin arcu, sed mattis orci gravida vel. Donec luctus turpis."
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text = AnnotatedString(text),
+ style = TextStyle(fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Ellipsis,
+ autoSize = AutoSize.StepBased(20.sp, 51.sp, 1.sp)
+ )
+ .also { it.density = density }
+
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = layoutCache.textLayoutResult
+ // Without ellipsis logic, the text would overflow with a height of 600.
+ // This shouldn't overflow due to the ellipsis logic.
+ // hasVisualOverflow unreliable here due to ellipsis logic. We'll test height manually
+ // instead
+ assertThat(layoutResult.didOverflowWidth).isFalse()
+ assertThat(layoutResult.multiParagraph.height).isEqualTo(100)
+ assertThat(layoutResult.isLineEllipsized(4)).isTrue()
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_visibleOverflow_doesOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text =
+ AnnotatedString(
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec egestas " +
+ "sollicitudin arcu, sed mattis orci gravida vel. Donec luctus turpis."
+ ),
+ style = TextStyle(fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Visible,
+ autoSize = AutoSize.StepBased(20.sp, 51.sp, 1.sp)
+ )
+ .also { it.density = density }
+
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = layoutCache.textLayoutResult
+ // this should overflow
+ assertThat(layoutResult.hasVisualOverflow).isTrue()
+ assertThat(layoutResult.multiParagraph.height).isEqualTo(600)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_em_checkOverflowAndHeight() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text = AnnotatedString(text),
+ style = TextStyle(fontSize = 5.sp, fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Ellipsis,
+ autoSize = AutoSizePreset(arrayOf(5.12.em)) // = 25.6sp
+ )
+ .also { it.density = density }
+
+ // 5.12.em / 25.6.sp shouldn't overflow
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ var layoutResult = layoutCache.textLayoutResult
+ assertThat(layoutResult.hasVisualOverflow).isFalse()
+ assertThat(layoutResult.multiParagraph.height).isEqualTo(100)
+
+ layoutCache.updateAutoSize(
+ text = text,
+ fontSize = 5.sp,
+ autoSize = AutoSizePreset(arrayOf(5.14.em))
+ )
+
+ // 5.14 .em / 25.7.sp should overflow
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ layoutResult = layoutCache.textLayoutResult
+ assertThat(layoutResult.hasVisualOverflow).isTrue()
+ assertThat(layoutResult.multiParagraph.height).isEqualTo(1000)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun autoSize_toPx_em_style_fontSize_is_em_throws() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text = AnnotatedString("Hello World"),
+ style = TextStyle(fontSize = 0.01.em, fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizePreset(arrayOf(2.em))
+ )
+ .also { it.density = density }
+
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_em_style_fontSize_is_unspecified_checkOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text = AnnotatedString(text),
+ style = TextStyle(fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizePreset(arrayOf(1.em))
+ )
+ .also { it.density = density }
+
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ var layoutResult = layoutCache.textLayoutResult
+ // doesn't overflow
+ assertThat(layoutResult.hasVisualOverflow).isFalse()
+
+ layoutCache.updateAutoSize(
+ text = text,
+ fontSize = TextUnit.Unspecified,
+ AutoSizePreset(arrayOf(2.em))
+ )
+
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ layoutResult = layoutCache.textLayoutResult
+ // does overflow
+ assertThat(layoutResult.hasVisualOverflow).isTrue()
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_em_withoutToPx_checkOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text = AnnotatedString(text),
+ style = TextStyle(fontSize = 1.em, fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizeWithoutToPx(2.em)
+ )
+ .also { it.density = density }
+
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ var layoutResult = layoutCache.textLayoutResult
+ // this shouldn't overflow
+ assertThat(layoutResult.hasVisualOverflow).isFalse()
+
+ layoutCache.updateAutoSize(
+ text = text,
+ fontSize = 1.em,
+ autoSize = AutoSizeWithoutToPx(3.em)
+ )
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ layoutResult = layoutCache.textLayoutResult
+ // this should overflow
+ assertThat(layoutResult.hasVisualOverflow).isTrue()
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_em_withoutToPx_unspecifiedStyleFontSize_checkOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text = AnnotatedString(text),
+ style = TextStyle(fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizeWithoutToPx(1.em)
+ )
+ .also { it.density = density }
+
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ var layoutResult = layoutCache.textLayoutResult
+ // this shouldn't overflow
+ assertThat(layoutResult.hasVisualOverflow).isFalse()
+
+ layoutCache.updateAutoSize(
+ text = text,
+ fontSize = TextUnit.Unspecified,
+ autoSize = AutoSizeWithoutToPx(2.em)
+ )
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ layoutResult = layoutCache.textLayoutResult
+ // this should overflow
+ assertThat(layoutResult.hasVisualOverflow).isTrue()
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_minLines_greaterThan_1_checkOverflowAndHeight() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+
+ val layoutCache =
+ MultiParagraphLayoutCache(
+ text = AnnotatedString("H"),
+ style = TextStyle(fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ minLines = 2,
+ autoSize = AutoSize.StepBased(20.sp, 51.sp, 1.sp)
+ )
+ .also { it.density = density }
+
+ layoutCache.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = layoutCache.textLayoutResult
+ assertThat(layoutResult.hasVisualOverflow).isFalse()
+ assertThat(layoutResult.multiParagraph.height)
+ .isAtMost(55) // this value is different between
+ // different API levels. Either 51 or 52. Using isAtMost to anticipate future permutations.
+ }
+
+ @Test
fun maxHeight_hasSameHeight_asParagraph() {
val text = buildAnnotatedString {
for (i in 1..100 step 10) {
@@ -354,4 +706,73 @@
.also { it.density = density }
subject.layoutWithConstraints(Constraints(), LayoutDirection.Ltr)
}
+
+ private fun MultiParagraphLayoutCache.updateAutoSize(
+ text: String,
+ fontSize: TextUnit,
+ autoSize: AutoSize
+ ) =
+ update(
+ text = AnnotatedString(text),
+ style = TextStyle(fontSize = fontSize, fontFamily = fontFamily),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ softWrap = true,
+ maxLines = Int.MAX_VALUE,
+ minLines = DefaultMinLines,
+ placeholders = null,
+ autoSize = autoSize
+ )
+
+ /**
+ * Version of AutoSize that takes in an array and attempts to find the largest font size in the
+ * array that doesn't overflow. If this is not found, `100.sp` will be returned
+ *
+ * @param presets The array of font sizes to be checked
+ */
+ private class AutoSizePreset(private val presets: Array<TextUnit>) : AutoSize {
+ override fun FontSizeSearchScope.getFontSize(): TextUnit {
+ var optimalFontSize = 0.sp
+ for (size in presets) {
+ if (
+ size.toPx() > optimalFontSize.toPx() &&
+ !performLayoutAndGetOverflow(size.toPx().toSp())
+ ) {
+ optimalFontSize = size
+ }
+ }
+ return if (optimalFontSize != 0.sp) optimalFontSize else 100.sp
+ // 100.sp is the font size returned when all sizes in the presets array overflow
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is AutoSizePreset) return false
+
+ return presets.contentEquals(other.presets)
+ }
+
+ override fun hashCode(): Int {
+ return presets.contentHashCode()
+ }
+ }
+
+ private class AutoSizeWithoutToPx(private val fontSize: TextUnit) : AutoSize {
+ override fun FontSizeSearchScope.getFontSize(): TextUnit {
+ // if there is overflow then 100.sp is returned. Otherwise 0.sp is returned
+ if (performLayoutAndGetOverflow(fontSize)) return 100.sp
+ return 0.sp
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is AutoSizeWithoutToPx) return false
+
+ return fontSize == other.fontSize
+ }
+
+ override fun hashCode(): Int {
+ return fontSize.hashCode()
+ }
+ }
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeInvalidationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeInvalidationTest.kt
index 935e686..fbe6b04 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeInvalidationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeInvalidationTest.kt
@@ -29,7 +29,9 @@
softWrap = params.softWrap,
fontFamilyResolver = params.fontFamilyResolver,
overflow = params.overflow,
- placeholders = null
+ placeholders = null,
+ // TODO(b/364657660): Give this a non-null value when AutoSize becomes public
+ autoSize = null
)
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/AutoSizeTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/AutoSizeTest.kt
index ba7fb8d..7cc941f 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/AutoSizeTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/AutoSizeTest.kt
@@ -126,13 +126,21 @@
}
@Test
- fun stepBased_getFontSize_overflowsWhenFontSizeIsGreaterThan60Px() {
+ fun stepBased_getFontSize_cappedAtMaxSize_beforeOverflow() {
val autoSize = AutoSize.StepBased(12.sp, 112.sp, 0.25.sp)
val searchScope: FontSizeSearchScope = OverflowsWhenFontSizeIsGreaterThan60px()
with(autoSize) { assertThat(searchScope.getFontSize().value).isEqualTo(60) }
}
@Test
+ fun stepBased_getFontSize_searchRangeMidpoint_overflows() {
+ val autoSize = AutoSize.StepBased(0.sp, 100.sp, 70.sp)
+ val searchScope: FontSizeSearchScope = OverflowsWhenFontSizeIsGreaterThan60px()
+ // Here we're testing when (max - min) / 2 overflows and min doesn't overflow
+ with(autoSize) { assertThat(searchScope.getFontSize().value) }.isEqualTo(0)
+ }
+
+ @Test
fun stepBased_getFontSize_differentStepSizes() {
val autoSize1 = AutoSize.StepBased(10.sp, 100.sp, 10.sp)
val autoSize2 = AutoSize.StepBased(10.sp, 100.sp, 20.sp)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 5aacedf..34391a9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -56,6 +56,7 @@
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.verticalScrollAxisRange
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastRoundToInt
/**
@@ -364,7 +365,7 @@
state.maxValue = side
state.viewportSize = if (isVertical) height else width
return layout(width, height) {
- val scroll = state.value.coerceIn(0, side)
+ val scroll = state.value.fastCoerceIn(0, side)
val absScroll = if (reverseScrolling) scroll - side else -scroll
val xOffset = if (isVertical) 0 else absScroll
val yOffset = if (isVertical) absScroll else 0
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/shape/CornerSize.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/shape/CornerSize.kt
index 768a7f0..bf80f49 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/shape/CornerSize.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/shape/CornerSize.kt
@@ -18,6 +18,7 @@
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
+import androidx.compose.foundation.internal.throwIllegalArgumentException
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Size
@@ -91,7 +92,7 @@
) : CornerSize, InspectableValue {
init {
if (percent < 0 || percent > 100) {
- throw IllegalArgumentException("The percent should be in the range of [0, 100]")
+ throwIllegalArgumentException("The percent should be in the range of [0, 100]")
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/AutoSize.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/AutoSize.kt
index c900e10..98a3f2c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/AutoSize.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/AutoSize.kt
@@ -191,7 +191,7 @@
current = (min + max) / 2
}
// used size minus minFontSize must be divisible by stepSize
- current = (floor((current - smallest) / stepSize) * stepSize + smallest)
+ current = (floor((min - smallest) / stepSize) * stepSize + smallest)
// try the next size up and see if it fits
if (
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
index e680d51..ee5fbf9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
@@ -487,6 +487,7 @@
onPlaceholderLayout,
null,
color,
+ null,
onShowTranslation
)
return this then Modifier /* selection position */ then staticTextModifier
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
index aaa4e25..966a6fa 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
@@ -16,7 +16,9 @@
package androidx.compose.foundation.text.modifiers
+import androidx.compose.foundation.text.AutoSize
import androidx.compose.foundation.text.DefaultMinLines
+import androidx.compose.foundation.text.FontSizeSearchScope
import androidx.compose.foundation.text.ceilToIntPx
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.MultiParagraph
@@ -32,7 +34,9 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.constrain
+import androidx.compose.ui.unit.sp
import kotlin.math.min
/**
@@ -52,6 +56,7 @@
private var maxLines: Int = Int.MAX_VALUE,
private var minLines: Int = DefaultMinLines,
private var placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
+ private var autoSize: AutoSize? = null
) {
/** Convert min max lines into actual constraints */
private var mMinLinesConstrainer: MinLinesConstrainer? = null
@@ -97,6 +102,16 @@
/** Output height for last call to [intrinsicHeight] at [cachedIntrinsicHeightInputWidth] */
private var cachedIntrinsicHeight: Int = -1
+ /** Backing property for [fontSizeSearchScope] */
+ private var _fontSizeSearchScope: FontSizeSearchScopeImpl? = null
+
+ /** Used to get the font size if AutoSize is enabled and perform layout with many font sizes */
+ private val fontSizeSearchScope: FontSizeSearchScopeImpl
+ get() {
+ if (_fontSizeSearchScope == null) _fontSizeSearchScope = FontSizeSearchScopeImpl()
+ return _fontSizeSearchScope!!
+ }
+
/** The last computed TextLayoutResult, or throws if not initialized. */
val textLayoutResult: TextLayoutResult
get() =
@@ -114,16 +129,7 @@
fun layoutWithConstraints(constraints: Constraints, layoutDirection: LayoutDirection): Boolean {
val finalConstraints =
if (minLines > 1) {
- val localMin =
- MinLinesConstrainer.from(
- mMinLinesConstrainer,
- layoutDirection,
- style,
- density!!,
- fontFamilyResolver
- )
- .also { mMinLinesConstrainer = it }
- localMin.coerceMinLines(inConstraints = constraints, minLines = minLines)
+ useMinLinesConstrainer(constraints, layoutDirection)
} else {
constraints
}
@@ -138,12 +144,50 @@
)
return true
}
+ if (autoSize != null) {
+ autoSize!!.performAutoSize(finalConstraints, layoutDirection).also {
+ style = style.copy(fontSize = it)
+ }
+ }
+
val multiParagraph = layoutText(finalConstraints, layoutDirection)
layoutCache = textLayoutResult(layoutDirection, finalConstraints, multiParagraph)
return true
}
+ private fun useMinLinesConstrainer(
+ constraints: Constraints,
+ layoutDirection: LayoutDirection
+ ): Constraints {
+ val localMin =
+ MinLinesConstrainer.from(
+ mMinLinesConstrainer,
+ layoutDirection,
+ style,
+ density!!,
+ fontFamilyResolver
+ )
+ .also { mMinLinesConstrainer = it }
+ return localMin.coerceMinLines(inConstraints = constraints, minLines = minLines)
+ }
+
+ private fun AutoSize.performAutoSize(
+ finalConstraints: Constraints,
+ layoutDirection: LayoutDirection
+ ): TextUnit {
+ fontSizeSearchScope.originalFontSize = style.fontSize
+ fontSizeSearchScope.layoutDirection = layoutDirection
+ fontSizeSearchScope.constraints = finalConstraints
+
+ var optimalFontSize = fontSizeSearchScope.getFontSize()
+ if (optimalFontSize.isEm) {
+ optimalFontSize = fontSizeSearchScope.originalFontSize * optimalFontSize.value
+ }
+
+ return optimalFontSize
+ }
+
private fun textLayoutResult(
layoutDirection: LayoutDirection,
finalConstraints: Constraints,
@@ -194,7 +238,8 @@
softWrap: Boolean,
maxLines: Int,
minLines: Int,
- placeholders: List<AnnotatedString.Range<Placeholder>>?
+ placeholders: List<AnnotatedString.Range<Placeholder>>?,
+ autoSize: AutoSize?
) {
this.text = text
this.style = style
@@ -204,6 +249,7 @@
this.maxLines = maxLines
this.minLines = minLines
this.placeholders = placeholders
+ this.autoSize = autoSize
markDirty()
}
@@ -257,7 +303,6 @@
overflow,
localParagraphIntrinsics.maxIntrinsicWidth
),
- // This is a fallback behavior for ellipsis. Native
maxLines = finalMaxLines(softWrap, overflow, maxLines),
overflow = overflow
)
@@ -319,4 +364,93 @@
fun minIntrinsicWidth(layoutDirection: LayoutDirection): Int {
return setLayoutDirection(layoutDirection).minIntrinsicWidth.ceilToIntPx()
}
+
+ /** [MultiParagraph] specific implementation of [FontSizeSearchScope] */
+ private inner class FontSizeSearchScopeImpl : FontSizeSearchScope {
+ /** Constraints that will be used to layout the text */
+ var constraints: Constraints = Constraints.fixed(0, 0)
+
+ /** The layout direction of the text */
+ var layoutDirection: LayoutDirection = LayoutDirection.Ltr
+
+ /** The font size that is initially provided in [style] */
+ var originalFontSize: TextUnit = TextUnit.Unspecified
+
+ private var resolvedStyle: TextStyle = resolveDefaults(style, layoutDirection)
+
+ // override Density attributes
+ override val density
+ get() = [email protected]!!.density
+
+ override val fontScale
+ get() = [email protected]!!.fontScale
+
+ override fun performLayoutAndGetOverflow(fontSize: TextUnit): Boolean {
+
+ var usedFontSize = fontSize
+ if (fontSize.isEm) {
+ if (originalFontSize == TextUnit.Unspecified) {
+ // Hardcoding DefaultFontSize as this is private in SpanStyle
+ // TODO(b/364858402): Make DefaultFontSize public
+ originalFontSize = DefaultFontSize
+ }
+ usedFontSize = originalFontSize * fontSize.value
+ }
+
+ val usedStyle = resolvedStyle.copy(fontSize = usedFontSize)
+ if (minLines > 1) {
+ constraints = useMinLinesConstrainer(constraints, layoutDirection)
+ }
+
+ val localParagraphIntrinsics =
+ MultiParagraphIntrinsics(
+ annotatedString = text,
+ style = usedStyle,
+ density = [email protected]!!,
+ fontFamilyResolver = fontFamilyResolver,
+ placeholders = placeholders.orEmpty()
+ )
+
+ val localMultiParagraph =
+ MultiParagraph(
+ intrinsics = localParagraphIntrinsics,
+ constraints =
+ finalConstraints(
+ constraints,
+ softWrap,
+ TextOverflow.Clip,
+ localParagraphIntrinsics.maxIntrinsicWidth
+ ),
+ maxLines = finalMaxLines(softWrap, overflow, maxLines),
+ overflow = TextOverflow.Clip
+ )
+ val localSize =
+ constraints.constrain(
+ IntSize(
+ localMultiParagraph.width.ceilToIntPx(),
+ localMultiParagraph.height.ceilToIntPx()
+ )
+ )
+ return localSize.width < localMultiParagraph.width ||
+ localSize.height < localMultiParagraph.height
+ }
+
+ override fun TextUnit.toPx(): Float {
+ if (isEm) {
+ check(!originalFontSize.isEm) {
+ "AutoSize -> toPx(): Cannot convert Em to Px when style.fontSize is Em\n" +
+ "Declare the composable's style.fontSize with Sp units instead."
+ }
+ if (originalFontSize == TextUnit.Unspecified) {
+ // Hardcoding DefaultFontSize as this is private in SpanStyle
+ // TODO(b/364858402): Make DefaultFontSize public
+ originalFontSize = DefaultFontSize
+ }
+ return originalFontSize.toPx() * value
+ }
+ return toDp().toPx()
+ }
+ }
}
+
+private val DefaultFontSize = 14.sp
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
index 482ba7d..8ee7aa4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
@@ -358,7 +358,7 @@
val annotatedString = AnnotatedString(text)
paragraph ?: return null
paragraphIntrinsics ?: return null
- val finalConstraints = prevConstraints.copy(minWidth = 0, minHeight = 0)
+ val finalConstraints = prevConstraints.copyMaxDimensions()
// and redo layout with MultiParagraph
return TextLayoutResult(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringElement.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringElement.kt
index 1e878ac..6ec5467 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringElement.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringElement.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.text.modifiers
+import androidx.compose.foundation.text.AutoSize
import androidx.compose.foundation.text.DefaultMinLines
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.ColorProducer
@@ -41,7 +42,8 @@
private val placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
private val onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
private val selectionController: SelectionController? = null,
- private val color: ColorProducer? = null
+ private val color: ColorProducer? = null,
+ private val autoSize: AutoSize? = null
) : ModifierNodeElement<SelectableTextAnnotatedStringNode>() {
override fun create(): SelectableTextAnnotatedStringNode =
@@ -57,7 +59,8 @@
placeholders,
onPlaceholderLayout,
selectionController,
- color
+ color,
+ autoSize
)
override fun update(node: SelectableTextAnnotatedStringNode) {
@@ -73,7 +76,8 @@
onTextLayout = onTextLayout,
onPlaceholderLayout = onPlaceholderLayout,
selectionController = selectionController,
- color = color
+ color = color,
+ autoSize = autoSize
)
}
@@ -90,6 +94,7 @@
// these are equally unlikely to change
if (fontFamilyResolver != other.fontFamilyResolver) return false
+ if (autoSize != other.autoSize) return false
if (onTextLayout !== other.onTextLayout) return false
if (overflow != other.overflow) return false
if (softWrap != other.softWrap) return false
@@ -115,6 +120,7 @@
result = 31 * result + (placeholders?.hashCode() ?: 0)
result = 31 * result + (onPlaceholderLayout?.hashCode() ?: 0)
result = 31 * result + (selectionController?.hashCode() ?: 0)
+ result = 31 * result + (autoSize?.hashCode() ?: 0)
result = 31 * result + (color?.hashCode() ?: 0)
return result
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
index c0be0a5..1dbe8d6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text.modifiers
import androidx.compose.foundation.internal.requirePreconditionNotNull
+import androidx.compose.foundation.text.AutoSize
import androidx.compose.foundation.text.DefaultMinLines
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.ColorProducer
@@ -58,6 +59,7 @@
onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
private var selectionController: SelectionController? = null,
overrideColor: ColorProducer? = null,
+ autoSize: AutoSize? = null,
private var onShowTranslation: ((TextAnnotatedStringNode.TextSubstitutionValue) -> Unit)? = null
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode {
@@ -76,6 +78,7 @@
onPlaceholderLayout = onPlaceholderLayout,
selectionController = selectionController,
overrideColor = overrideColor,
+ autoSize = autoSize,
onShowTranslation = onShowTranslation
)
)
@@ -129,7 +132,8 @@
onTextLayout: ((TextLayoutResult) -> Unit)?,
onPlaceholderLayout: ((List<Rect?>) -> Unit)?,
selectionController: SelectionController?,
- color: ColorProducer?
+ color: ColorProducer?,
+ autoSize: AutoSize?
) {
delegate.doInvalidations(
drawChanged = delegate.updateDraw(color, style),
@@ -142,7 +146,8 @@
maxLines = maxLines,
softWrap = softWrap,
fontFamilyResolver = fontFamilyResolver,
- overflow = overflow
+ overflow = overflow,
+ autoSize = autoSize
),
callbacksChanged =
delegate.updateCallbacks(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt
index ff05c9b..8db153af 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.text.modifiers
+import androidx.compose.foundation.text.AutoSize
import androidx.compose.foundation.text.DefaultMinLines
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.ColorProducer
@@ -46,6 +47,7 @@
private val onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
private val selectionController: SelectionController? = null,
private val color: ColorProducer? = null,
+ private val autoSize: AutoSize? = null,
private val onShowTranslation: ((TextAnnotatedStringNode.TextSubstitutionValue) -> Unit)? = null
) : ModifierNodeElement<TextAnnotatedStringNode>() {
@@ -63,6 +65,7 @@
onPlaceholderLayout,
selectionController,
color,
+ autoSize,
onShowTranslation
)
@@ -78,7 +81,8 @@
maxLines = maxLines,
softWrap = softWrap,
fontFamilyResolver = fontFamilyResolver,
- overflow = overflow
+ overflow = overflow,
+ autoSize = autoSize
),
callbacksChanged =
node.updateCallbacks(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
index ab477e8..de5f472 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.text.modifiers
+import androidx.compose.foundation.text.AutoSize
import androidx.compose.foundation.text.DefaultMinLines
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -78,6 +79,7 @@
private var onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
private var selectionController: SelectionController? = null,
private var overrideColor: ColorProducer? = null,
+ private var autoSize: AutoSize? = null,
private var onShowTranslation: ((TextSubstitutionValue) -> Unit)? = null
) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
@Suppress("PrimitiveInCollection")
@@ -96,7 +98,8 @@
softWrap,
maxLines,
minLines,
- placeholders
+ placeholders,
+ autoSize
)
}
return _layoutCache!!
@@ -153,7 +156,8 @@
maxLines: Int,
softWrap: Boolean,
fontFamilyResolver: FontFamily.Resolver,
- overflow: TextOverflow
+ overflow: TextOverflow,
+ autoSize: AutoSize?
): Boolean {
var changed: Boolean
@@ -190,6 +194,11 @@
changed = true
}
+ if (this.autoSize != autoSize) {
+ this.autoSize = autoSize
+ changed = true
+ }
+
return changed
}
@@ -241,7 +250,8 @@
softWrap = softWrap,
maxLines = maxLines,
minLines = minLines,
- placeholders = placeholders
+ placeholders = placeholders,
+ autoSize = autoSize
)
}
@@ -289,7 +299,8 @@
softWrap,
maxLines,
minLines,
- placeholders
+ placeholders,
+ autoSize
) ?: return false
} else {
val newTextSubstitution = TextSubstitutionValue(text, updatedText)
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/AccessibilityNodeInspector.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/AccessibilityNodeInspector.kt
index 3f3aa55..f6394fe 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/AccessibilityNodeInspector.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/AccessibilityNodeInspector.kt
@@ -640,11 +640,11 @@
measurePolicy = { measurables, constraints ->
val contentPaddingPx = contentPadding.roundToPx()
val (keyMeasurable, valueMeasurable) = measurables
- val keyConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val keyConstraints = constraints.copyMaxDimensions()
// contentPadding will either act as the spacing between items if they fit on the same
// line, or indent if content wraps, so inset the constraints either way.
val valueConstraints =
- constraints.copy(minWidth = 0, minHeight = 0).offset(horizontal = -contentPaddingPx)
+ constraints.copyMaxDimensions().offset(horizontal = -contentPaddingPx)
val keyPlaceable = keyMeasurable.measure(keyConstraints)
val valuePlaceable = valueMeasurable.measure(valueConstraints)
val wrap =
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
index 8e9bc84..2550ab9 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
@@ -386,7 +386,7 @@
}
}
val calculateBackLayerConstraints: (Constraints) -> Constraints = {
- it.copy(minWidth = 0, minHeight = 0).offset(vertical = -headerHeightPx.roundToInt())
+ it.copyMaxDimensions().offset(vertical = -headerHeightPx.roundToInt())
}
val state = scaffoldState.anchoredDraggableState
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
index 76fa17e6..d09b8f2 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
@@ -487,7 +487,7 @@
constraints ->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
val sheetPlaceables = sheetMeasurables.fastMap { it.measure(looseConstraints) }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index 546b0a8..07e05d8 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -32,11 +32,11 @@
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.KeyboardActionHandler
import androidx.compose.foundation.text.input.OutputTransformation
+import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldLineLimits.MultiLine
import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
import androidx.compose.foundation.text.input.TextFieldState
-import androidx.compose.foundation.text.input.toTextFieldBuffer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -219,10 +219,13 @@
if (outputTransformation == null) {
state.text.toString()
} else {
- val buffer = state.toTextFieldBuffer()
+ // TODO: use constructor to create TextFieldBuffer from TextFieldState when
+ // available
+ lateinit var buffer: TextFieldBuffer
+ state.edit { buffer = this }
// after edit completes, mutations on buffer are ineffective
with(outputTransformation) { buffer.transformOutput() }
- buffer.toString()
+ buffer.asCharSequence().toString()
}
TextFieldDefaults.OutlinedTextFieldDecorationBox(
@@ -769,7 +772,7 @@
val bottomPadding = paddingValues.calculateBottomPadding().roundToPx()
// measure leading icon
- val relaxedConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val relaxedConstraints = constraints.copyMaxDimensions()
val leadingPlaceable =
measurables.fastFirstOrNull { it.layoutId == LeadingId }?.measure(relaxedConstraints)
occupiedSpaceHorizontally += widthOrZero(leadingPlaceable)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
index b7b30dc..dc7e6ce 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
@@ -378,7 +378,7 @@
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
val topBarPlaceables =
subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
index ce10bc3..6a253ba 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
@@ -34,11 +34,11 @@
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.KeyboardActionHandler
import androidx.compose.foundation.text.input.OutputTransformation
+import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldLineLimits.MultiLine
import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
import androidx.compose.foundation.text.input.TextFieldState
-import androidx.compose.foundation.text.input.toTextFieldBuffer
import androidx.compose.material.TextFieldDefaults.indicatorLine
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -226,10 +226,13 @@
if (outputTransformation == null) {
state.text.toString()
} else {
- val buffer = state.toTextFieldBuffer()
+ // TODO: use constructor to create TextFieldBuffer from TextFieldState when
+ // available
+ lateinit var buffer: TextFieldBuffer
+ state.edit { buffer = this }
// after edit completes, mutations on buffer are ineffective
with(outputTransformation) { buffer.transformOutput() }
- buffer.toString()
+ buffer.asCharSequence().toString()
}
TextFieldDefaults.TextFieldDecorationBox(
@@ -722,7 +725,7 @@
var occupiedSpaceHorizontally = 0
// measure leading icon
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
val leadingPlaceable =
measurables.fastFirstOrNull { it.layoutId == LeadingId }?.measure(looseConstraints)
occupiedSpaceHorizontally += widthOrZero(leadingPlaceable)
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
index 40758e0..0976a74 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
@@ -155,7 +155,7 @@
Box(Modifier.layoutId(NavigationSuiteLayoutIdTag)) { navigationSuite() }
Box(Modifier.layoutId(ContentLayoutIdTag)) { content() }
}) { measurables, constraints ->
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
// Find the navigation suite composable through it's layoutId tag
val navigationPlaceable =
measurables
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 80d67dd..0953923a 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -3094,10 +3094,10 @@
method public suspend Object? animateToHidden(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? animateToThreshold(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @FloatRange(from=0.0) public float getDistanceFraction();
- method public default boolean isAnimating();
+ method public boolean isAnimating();
method public suspend Object? snapTo(@FloatRange(from=0.0) float targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
property @FloatRange(from=0.0) public abstract float distanceFraction;
- property public default boolean isAnimating;
+ property public abstract boolean isAnimating;
}
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 80d67dd..0953923a 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -3094,10 +3094,10 @@
method public suspend Object? animateToHidden(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? animateToThreshold(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @FloatRange(from=0.0) public float getDistanceFraction();
- method public default boolean isAnimating();
+ method public boolean isAnimating();
method public suspend Object? snapTo(@FloatRange(from=0.0) float targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
property @FloatRange(from=0.0) public abstract float distanceFraction;
- property public default boolean isAnimating;
+ property public abstract boolean isAnimating;
}
}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
index fc91ad3..1e7cc00 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
@@ -366,6 +366,9 @@
override val distanceFraction
get() = anim.value
+ override val isAnimating: Boolean
+ get() = anim.isRunning
+
override suspend fun animateToThreshold() {
anim.animateTo(1f, spring(dampingRatio = Spring.DampingRatioHighBouncy))
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt
index 71e8228..0cfc573 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt
@@ -16,14 +16,6 @@
package androidx.compose.material3
-import android.os.Build
-import android.view.InputDevice
-import android.view.MotionEvent
-import android.view.MotionEvent.ACTION_DOWN
-import android.view.MotionEvent.ACTION_MOVE
-import android.view.MotionEvent.CLASSIFICATION_DEEP_PRESS
-import android.view.MotionEvent.CLASSIFICATION_NONE
-import android.view.View
import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
@@ -37,7 +29,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInWindow
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.SemanticsMatcher
@@ -61,7 +52,6 @@
import androidx.compose.ui.window.PopupPositionProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
@@ -430,107 +420,6 @@
assertThat(state.isVisible).isFalse()
}
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
- @Test
- fun plainTooltip_longPress_deepPress_showsTooltip() {
- lateinit var view: View
- lateinit var state: TooltipState
- var changedToVisible = false
- rule.mainClock.autoAdvance = false
- rule.setMaterialContent(lightColorScheme()) {
- view = LocalView.current
- state = rememberTooltipState()
- LaunchedEffect(true) {
- snapshotFlow { state.isVisible }
- .collectLatest {
- if (it) {
- changedToVisible = true
- }
- }
- }
- Box(Modifier.testTag("tooltip")) {
- PlainTooltipTest(tooltipContent = { Text(text = "Test") }, tooltipState = state)
- }
- }
-
- assertThat(changedToVisible).isFalse()
-
- val pointerProperties =
- arrayOf(
- MotionEvent.PointerProperties().also {
- it.id = 0
- it.toolType = MotionEvent.TOOL_TYPE_FINGER
- }
- )
-
- val downEvent =
- MotionEvent.obtain(
- /* downTime = */ 0,
- /* eventTime = */ 0,
- /* action = */ ACTION_DOWN,
- /* pointerCount = */ 1,
- /* pointerProperties = */ pointerProperties,
- /* pointerCoords = */ arrayOf(
- MotionEvent.PointerCoords().apply {
- x = 5f
- y = 5f
- }
- ),
- /* metaState = */ 0,
- /* buttonState = */ 0,
- /* xPrecision = */ 0f,
- /* yPrecision = */ 0f,
- /* deviceId = */ 0,
- /* edgeFlags = */ 0,
- /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
- /* displayId = */ 0,
- /* flags = */ 0,
- /* classification = */ CLASSIFICATION_NONE
- )
-
- view.dispatchTouchEvent(downEvent)
- rule.mainClock.advanceTimeBy(50)
-
- rule.runOnIdle {
- assertThat(changedToVisible).isFalse()
- assertThat(state.isVisible).isFalse()
- }
-
- val deepPressMoveEvent =
- MotionEvent.obtain(
- /* downTime = */ 0,
- /* eventTime = */ 50,
- /* action = */ ACTION_MOVE,
- /* pointerCount = */ 1,
- /* pointerProperties = */ pointerProperties,
- /* pointerCoords = */ arrayOf(
- MotionEvent.PointerCoords().apply {
- x = 10f
- y = 10f
- }
- ),
- /* metaState = */ 0,
- /* buttonState = */ 0,
- /* xPrecision = */ 0f,
- /* yPrecision = */ 0f,
- /* deviceId = */ 0,
- /* edgeFlags = */ 0,
- /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
- /* displayId = */ 0,
- /* flags = */ 0,
- /* classification = */ CLASSIFICATION_DEEP_PRESS
- )
-
- view.dispatchTouchEvent(deepPressMoveEvent)
- rule.mainClock.advanceTimeBy(50)
-
- // Even though the timeout didn't pass, the deep press should immediately show the tooltip
- rule.runOnIdle {
- assertThat(changedToVisible).isTrue()
- assertThat(state.isVisible).isTrue()
- }
- }
-
@Test
fun plainTooltip_longPress_keepsTooltipVisible() {
lateinit var state: TooltipState
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshIndicatorScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshIndicatorScreenshotTest.kt
index 6acbb51..53e56a0 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshIndicatorScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshIndicatorScreenshotTest.kt
@@ -111,6 +111,9 @@
override val distanceFraction: Float
get() = 1f
+ override val isAnimating: Boolean
+ get() = false
+
override suspend fun animateToThreshold() {}
override suspend fun animateToHidden() {}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshStateImplTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshStateImplTest.kt
index 08776e6..c79c5c0 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshStateImplTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshStateImplTest.kt
@@ -104,6 +104,9 @@
override val distanceFraction: Float
get() = distanceFractionState
+ override val isAnimating: Boolean
+ get() = false
+
override suspend fun animateToThreshold() {}
override suspend fun animateToHidden() {}
@@ -148,6 +151,9 @@
override val distanceFraction: Float
get() = distanceFractionState
+ override val isAnimating: Boolean
+ get() = false
+
override suspend fun animateToThreshold() {}
override suspend fun animateToHidden() {}
@@ -199,6 +205,9 @@
override val distanceFraction: Float
get() = distanceFractionState
+ override val isAnimating: Boolean
+ get() = false
+
override suspend fun animateToThreshold() {}
override suspend fun animateToHidden() {}
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt
index 4040b26..cbfaeb3 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt
@@ -18,7 +18,6 @@
package androidx.compose.material3.internal
-import android.view.MotionEvent
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.R
import androidx.compose.foundation.gestures.awaitEachGesture
@@ -31,13 +30,10 @@
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerType
-import androidx.compose.ui.input.pointer.changedToUp
-import androidx.compose.ui.input.pointer.isOutOfBounds
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.LiveRegionMode
@@ -45,8 +41,6 @@
import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.paneTitle
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastAny
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
@@ -170,13 +164,20 @@
// Long press will finish before or after show so keep track of it, in a
// flow to handle both cases
val isLongPressedFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ val longPressTimeout = viewConfiguration.longPressTimeoutMillis
val pass = PointerEventPass.Initial
// wait for the first down press
val inputType = awaitFirstDown(pass = pass).type
if (inputType == PointerType.Touch || inputType == PointerType.Stylus) {
- if (waitForLongPress(pass = pass)) {
+ try {
+ // listen to if there is up gesture
+ // within the longPressTimeout limit
+ withTimeout(longPressTimeout) {
+ waitForUpOrCancellation(pass = pass)
+ }
+ } catch (_: PointerEventTimeoutCancellationException) {
// handle long press - Show the tooltip
launch(start = CoroutineStart.UNDISPATCHED) {
try {
@@ -195,8 +196,9 @@
// Long press may still be in progress
val upEvent = waitForUpOrCancellation(pass = pass)
upEvent?.consume()
+ } finally {
+ isLongPressedFlow.tryEmit(false)
}
- isLongPressedFlow.tryEmit(false)
}
}
}
@@ -242,45 +244,3 @@
)
}
} else this
-
-// TODO: b/305997392 move to use foundation API for tooltip gestures and remove this
-/** @return true if long press occurred, false otherwise */
-private suspend fun AwaitPointerEventScope.waitForLongPress(
- pass: PointerEventPass = PointerEventPass.Main
-): Boolean {
- var result = false
- try {
- withTimeout(viewConfiguration.longPressTimeoutMillis) {
- while (true) {
- val event = awaitPointerEvent(pass)
- if (event.changes.fastAll { it.changedToUp() }) {
- // All pointers are up
- break
- }
-
- if (event.classification == MotionEvent.CLASSIFICATION_DEEP_PRESS) {
- result = true
- break
- }
-
- if (
- event.changes.fastAny {
- it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)
- }
- ) {
- break
- }
-
- // Check for cancel by position consumption. We can look on the Final pass of the
- // existing pointer event because it comes after the pass we checked above.
- val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
- if (consumeCheck.changes.fastAny { it.isConsumed }) {
- break
- }
- }
- }
- } catch (_: PointerEventTimeoutCancellationException) {
- return true
- }
- return result
-}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
index 8ef2938..0b33de8 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
@@ -401,7 +401,7 @@
constraints ->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
val sheetPlaceables = bottomSheetMeasurables.fastMap { it.measure(looseConstraints) }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
index b426d89..e64764a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
@@ -2086,14 +2086,14 @@
val leadingIconPlaceable: Placeable? =
measurables
.fastFirstOrNull { it.layoutId == LeadingIconLayoutId }
- ?.measure(constraints.copy(minWidth = 0, minHeight = 0))
+ ?.measure(constraints.copyMaxDimensions())
val leadingIconWidth = leadingIconPlaceable.widthOrZero
val leadingIconHeight = leadingIconPlaceable.heightOrZero
val trailingIconPlaceable: Placeable? =
measurables
.fastFirstOrNull { it.layoutId == TrailingIconLayoutId }
- ?.measure(constraints.copy(minWidth = 0, minHeight = 0))
+ ?.measure(constraints.copyMaxDimensions())
val trailingIconWidth = trailingIconPlaceable.widthOrZero
val trailingIconHeight = trailingIconPlaceable.heightOrZero
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
index 717f48b..016d0a3 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
@@ -209,7 +209,7 @@
measurables
var currentTotalWidth = 0
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
val startPadding = ListItemStartPadding
val endPadding = ListItemEndPadding
val horizontalPadding = (startPadding + endPadding).roundToPx()
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
index 0a5aec8..cf00f34 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
@@ -553,7 +553,7 @@
@Suppress("NAME_SHADOWING")
// Ensure that the progress is >= 0. It may be negative on bouncy springs, for example.
val animationProgress = sizeAnimationProgress().coerceAtLeast(0f)
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
val iconPlaceable =
measurables.fastFirst { it.layoutId == IconLayoutIdTag }.measure(looseConstraints)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index 4ebaa83..466d9f1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -406,7 +406,7 @@
}
},
) { measurables, constraints ->
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
val placeables = measurables.fastMap { it.measure(looseConstraints) }
val width = placeables.fastMaxOfOrNull { it.width } ?: 0
val height = placeables.fastMaxOfOrNull { it.height } ?: 0
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
index 771d263..9d59164 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
@@ -582,7 +582,7 @@
constraints: Constraints
): MeasureResult {
@Suppress("NAME_SHADOWING") val indicatorAnimationProgress = indicatorAnimationProgress()
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
// When measuring icon, account for the indicator in its constraints.
val iconPlaceable =
measurables
@@ -674,7 +674,7 @@
constraints: Constraints
): MeasureResult {
@Suppress("NAME_SHADOWING") val indicatorAnimationProgress = indicatorAnimationProgress()
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
// When measuring icon, account for the indicator in its constraints.
val iconConstraints =
looseConstraints.offset(
@@ -778,7 +778,7 @@
): MeasureResult {
@Suppress("NAME_SHADOWING") val indicatorAnimationProgress = indicatorAnimationProgress()
val iconPositionProgressValue = iconPositionProgress()
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
val iconPlaceable =
measurables.fastFirst { it.layoutId == IconLayoutIdTag }.measure(looseConstraints)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
index c88ebfef..0a9cd95 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
@@ -554,7 +554,7 @@
@Suppress("NAME_SHADOWING")
// Ensure that the progress is >= 0. It may be negative on bouncy springs, for example.
val animationProgress = sizeAnimationProgress().coerceAtLeast(0f)
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
val iconPlaceable =
measurables.fastFirst { it.layoutId == IconLayoutIdTag }.measure(looseConstraints)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index e23b611..485a986 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -808,7 +808,7 @@
var occupiedSpaceVertically = 0
val bottomPadding = paddingValues.calculateBottomPadding().roundToPx()
- val relaxedConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val relaxedConstraints = constraints.copyMaxDimensions()
// measure leading icon
val leadingPlaceable =
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
index e6c27fd..c29179a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
@@ -140,7 +140,7 @@
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
val topBarPlaceables =
subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
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 63959a5..a2d11a5 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
@@ -422,7 +422,7 @@
},
modifier,
measurePolicy = { measurables, constraints ->
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
val leadingButtonPlaceable =
measurables
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 e630280..2aebf9b 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
@@ -803,7 +803,7 @@
var occupiedSpaceHorizontally = 0
var occupiedSpaceVertically = 0
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
// measure leading icon
val leadingPlaceable =
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 66dba58..2e9ff2c 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
@@ -27,12 +27,12 @@
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.OutputTransformation
+import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldDecorator
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldLineLimits.MultiLine
import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
import androidx.compose.foundation.text.input.TextFieldState
-import androidx.compose.foundation.text.input.toTextFieldBuffer
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material3.internal.CommonDecorationBox
@@ -181,10 +181,13 @@
val visualText =
if (outputTransformation == null) state.text
else {
- val buffer = state.toTextFieldBuffer()
+ // TODO: use constructor to create TextFieldBuffer from TextFieldState when
+ // available
+ lateinit var buffer: TextFieldBuffer
+ state.edit { buffer = this }
// after edit completes, mutations on buffer are ineffective
with(outputTransformation) { buffer.transformOutput() }
- buffer.toString()
+ buffer.asCharSequence()
}
CommonDecorationBox(
@@ -993,10 +996,13 @@
val visualText =
if (outputTransformation == null) state.text
else {
- val buffer = state.toTextFieldBuffer()
+ // TODO: use constructor to create TextFieldBuffer from TextFieldState when
+ // available
+ lateinit var buffer: TextFieldBuffer
+ state.edit { buffer = this }
// after edit completes, mutations on buffer are ineffective
with(outputTransformation) { buffer.transformOutput() }
- buffer.toString()
+ buffer.asCharSequence()
}
CommonDecorationBox(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
index b0fe245..933df32 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
@@ -1913,7 +1913,7 @@
) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
val radiusPx = radius.toPx()
- val itemConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val itemConstraints = constraints.copyMaxDimensions()
val placeables =
measurables
.fastFilter {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
index 3cbed88..768d6e2 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
@@ -260,7 +260,7 @@
if (itemsCount < 1) {
return layout(actualMinWidth, height) {}
}
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
var itemsMeasurables = measurables
var constraintsOffset = 0
@@ -305,10 +305,10 @@
)
)
)
- val maxIntrinsicWidth = it.maxIntrinsicWidth(constraintsOffset)
- if (expanded && expandedItemMaxWidth < maxIntrinsicWidth) {
+ val maxItemWidth = measuredItem.measuredWidth
+ if (expanded && expandedItemMaxWidth < maxItemWidth) {
expandedItemMaxWidth =
- maxIntrinsicWidth +
+ maxItemWidth +
(ExpandedRailHorizontalItemPadding * 2).roundToPx()
}
constraintsOffset = measuredItem.height
@@ -344,7 +344,8 @@
.roundToPx()
.coerceIn(
minimumValue = actualMinWidth,
- maximumValue = currentWidth
+ maximumValue =
+ currentWidth.coerceAtLeast(actualMinWidth)
)
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
index ba0cb07..5207cb7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
@@ -231,6 +231,9 @@
var threshold: Dp,
) : DelegatingNode(), CompositionLocalConsumerModifierNode, NestedScrollConnection {
+ override val shouldAutoInvalidate: Boolean
+ get() = false
+
private var nestedScrollNode: DelegatableNode =
nestedScrollModifierNode(
connection = this,
@@ -629,9 +632,11 @@
*/
@get:FloatRange(from = 0.0) val distanceFraction: Float
- /** Whether the state is currently animating */
+ /**
+ * whether the state is currently animating the indicator to the threshold offset, or back to
+ * the hidden offset
+ */
val isAnimating: Boolean
- get() = false
/**
* Animate the distance towards the anchor or threshold position, where the indicator will be
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/TestOnly.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/TestOnly.kt
index 0f57f58..fe01127 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/TestOnly.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/TestOnly.kt
@@ -17,7 +17,7 @@
package androidx.compose.runtime
@MustBeDocumented
-@Retention(AnnotationRetention.BINARY)
+@Retention(AnnotationRetention.SOURCE)
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.CONSTRUCTOR,
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.kt
index 34de83e..dac4652 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.kt
@@ -35,4 +35,6 @@
companion object Key : CoroutineContext.Key<SnapshotContextElement>
}
-internal expect class SnapshotContextElementImpl(snapshot: Snapshot) : SnapshotContextElement
+internal expect class SnapshotContextElementImpl(snapshot: Snapshot) : SnapshotContextElement {
+ override val key: CoroutineContext.Key<*>
+}
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/kotlinx/test/IgnoreJsTarget.kt b/compose/runtime/runtime/src/commonTest/kotlin/kotlinx/test/IgnoreJsTarget.kt
index 31e55dd..a2125ff 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/kotlinx/test/IgnoreJsTarget.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/kotlinx/test/IgnoreJsTarget.kt
@@ -15,4 +15,4 @@
*/
package kotlinx.test
-expect annotation class IgnoreJsTarget
+expect annotation class IgnoreJsTarget()
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.jvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.jvm.kt
index d27116f..60e770a 100644
--- a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.jvm.kt
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.jvm.kt
@@ -22,7 +22,7 @@
internal actual class SnapshotContextElementImpl
actual constructor(private val snapshot: Snapshot) :
SnapshotContextElement, ThreadContextElement<Snapshot?> {
- override val key: CoroutineContext.Key<*>
+ actual override val key: CoroutineContext.Key<*>
get() = SnapshotContextElement
override fun updateThreadContext(context: CoroutineContext): Snapshot? = snapshot.unsafeEnter()
diff --git a/compose/runtime/runtime/src/linuxx64StubsMain/kotlin/androidx/compose/runtime/TestOnly.linuxx64Stubs.kt b/compose/runtime/runtime/src/linuxx64StubsMain/kotlin/androidx/compose/runtime/TestOnly.linuxx64Stubs.kt
index 49d5da72..0a9efef 100644
--- a/compose/runtime/runtime/src/linuxx64StubsMain/kotlin/androidx/compose/runtime/TestOnly.linuxx64Stubs.kt
+++ b/compose/runtime/runtime/src/linuxx64StubsMain/kotlin/androidx/compose/runtime/TestOnly.linuxx64Stubs.kt
@@ -16,4 +16,12 @@
package androidx.compose.runtime
+@MustBeDocumented
+@Retention(AnnotationRetention.SOURCE)
+@Target(
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.CONSTRUCTOR,
+ AnnotationTarget.PROPERTY_GETTER,
+ AnnotationTarget.PROPERTY_SETTER
+)
actual annotation class TestOnly
diff --git a/compose/runtime/runtime/src/linuxx64StubsMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.linuxx64Stubs.kt b/compose/runtime/runtime/src/linuxx64StubsMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.linuxx64Stubs.kt
index 4763182..4ee02a1 100644
--- a/compose/runtime/runtime/src/linuxx64StubsMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.linuxx64Stubs.kt
+++ b/compose/runtime/runtime/src/linuxx64StubsMain/kotlin/androidx/compose/runtime/snapshots/SnapshotContextElement.linuxx64Stubs.kt
@@ -21,6 +21,6 @@
internal actual class SnapshotContextElementImpl
actual constructor(private val snapshot: Snapshot) : SnapshotContextElement {
- override val key: CoroutineContext.Key<*>
+ actual override val key: CoroutineContext.Key<*>
get() = implementedInJetBrainsFork()
}
diff --git a/compose/ui/ui-geometry/api/current.txt b/compose/ui/ui-geometry/api/current.txt
index 7cfe3bd..3a2dac5 100644
--- a/compose/ui/ui-geometry/api/current.txt
+++ b/compose/ui/ui-geometry/api/current.txt
@@ -2,18 +2,23 @@
package androidx.compose.ui.geometry {
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class CornerRadius {
+ ctor public CornerRadius(long packedValue);
method @androidx.compose.runtime.Stable public inline operator float component1();
method @androidx.compose.runtime.Stable public inline operator float component2();
method public long copy(optional float x, optional float y);
method @androidx.compose.runtime.Stable public operator long div(float operand);
- method public float getX();
- method public float getY();
+ method public long getPackedValue();
+ method public inline float getX();
+ method public inline float getY();
+ method @androidx.compose.runtime.Stable public inline boolean isCircular();
+ method @androidx.compose.runtime.Stable public inline boolean isZero();
method @androidx.compose.runtime.Stable public operator long minus(long other);
method @androidx.compose.runtime.Stable public operator long plus(long other);
method @androidx.compose.runtime.Stable public operator long times(float operand);
- method @androidx.compose.runtime.Stable public operator long unaryMinus();
- property @androidx.compose.runtime.Stable public final float x;
- property @androidx.compose.runtime.Stable public final float y;
+ method @androidx.compose.runtime.Stable public inline operator long unaryMinus();
+ property public final long packedValue;
+ property @androidx.compose.runtime.Stable public final inline float x;
+ property @androidx.compose.runtime.Stable public final inline float y;
field public static final androidx.compose.ui.geometry.CornerRadius.Companion Companion;
}
@@ -23,7 +28,7 @@
}
public final class CornerRadiusKt {
- method @androidx.compose.runtime.Stable public static long CornerRadius(float x, optional float y);
+ method @androidx.compose.runtime.Stable public static inline long CornerRadius(float x, optional float y);
method @androidx.compose.runtime.Stable public static long lerp(long start, long stop, float fraction);
}
diff --git a/compose/ui/ui-geometry/api/restricted_current.txt b/compose/ui/ui-geometry/api/restricted_current.txt
index 940e4ba..13de29e 100644
--- a/compose/ui/ui-geometry/api/restricted_current.txt
+++ b/compose/ui/ui-geometry/api/restricted_current.txt
@@ -2,18 +2,23 @@
package androidx.compose.ui.geometry {
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class CornerRadius {
+ ctor public CornerRadius(long packedValue);
method @androidx.compose.runtime.Stable public inline operator float component1();
method @androidx.compose.runtime.Stable public inline operator float component2();
method public long copy(optional float x, optional float y);
method @androidx.compose.runtime.Stable public operator long div(float operand);
- method public float getX();
- method public float getY();
+ method public long getPackedValue();
+ method public inline float getX();
+ method public inline float getY();
+ method @androidx.compose.runtime.Stable public inline boolean isCircular();
+ method @androidx.compose.runtime.Stable public inline boolean isZero();
method @androidx.compose.runtime.Stable public operator long minus(long other);
method @androidx.compose.runtime.Stable public operator long plus(long other);
method @androidx.compose.runtime.Stable public operator long times(float operand);
- method @androidx.compose.runtime.Stable public operator long unaryMinus();
- property @androidx.compose.runtime.Stable public final float x;
- property @androidx.compose.runtime.Stable public final float y;
+ method @androidx.compose.runtime.Stable public inline operator long unaryMinus();
+ property public final long packedValue;
+ property @androidx.compose.runtime.Stable public final inline float x;
+ property @androidx.compose.runtime.Stable public final inline float y;
field public static final androidx.compose.ui.geometry.CornerRadius.Companion Companion;
}
@@ -23,7 +28,7 @@
}
public final class CornerRadiusKt {
- method @androidx.compose.runtime.Stable public static long CornerRadius(float x, optional float y);
+ method @androidx.compose.runtime.Stable public static inline long CornerRadius(float x, optional float y);
method @androidx.compose.runtime.Stable public static long lerp(long start, long stop, float fraction);
}
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/CornerRadius.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/CornerRadius.kt
index c584335..02343a2 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/CornerRadius.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/CornerRadius.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.ui.geometry
import androidx.compose.runtime.Immutable
@@ -28,7 +30,7 @@
* and y axis respectively. By default the radius along the Y axis matches that of the given x-axis
* unless otherwise specified. Negative radii values are clamped to 0.
*/
-@Stable fun CornerRadius(x: Float, y: Float = x) = CornerRadius(packFloats(x, y))
+@Stable inline fun CornerRadius(x: Float, y: Float = x) = CornerRadius(packFloats(x, y))
/**
* A radius for either circular or elliptical (oval) shapes.
@@ -39,36 +41,49 @@
*/
@Immutable
@kotlin.jvm.JvmInline
-value class CornerRadius internal constructor(@PublishedApi internal val packedValue: Long) {
-
+value class CornerRadius(val packedValue: Long) {
/** The radius value on the horizontal axis. */
@Stable
- val x: Float
+ inline val x: Float
get() = unpackFloat1(packedValue)
/** The radius value on the vertical axis. */
@Stable
- val y: Float
+ inline val y: Float
get() = unpackFloat2(packedValue)
- @Suppress("NOTHING_TO_INLINE") @Stable inline operator fun component1(): Float = x
+ @Stable inline operator fun component1(): Float = x
- @Suppress("NOTHING_TO_INLINE") @Stable inline operator fun component2(): Float = y
+ @Stable inline operator fun component2(): Float = y
/**
* Returns a copy of this Radius instance optionally overriding the radius parameter for the x
* or y axis
*/
- fun copy(x: Float = this.x, y: Float = this.y) = CornerRadius(x, y)
+ fun copy(x: Float = unpackFloat1(packedValue), y: Float = unpackFloat2(packedValue)) =
+ CornerRadius(packFloats(x, y))
companion object {
-
/**
* A radius with [x] and [y] values set to zero.
*
* You can use [CornerRadius.Zero] with [RoundRect] to have right-angle corners.
*/
- @Stable val Zero: CornerRadius = CornerRadius(0.0f)
+ @Stable val Zero: CornerRadius = CornerRadius(0x0L)
+ }
+
+ /** Whether this corner radius is 0 in x, y, or both. */
+ @Stable
+ inline fun isZero(): Boolean {
+ // account for +/- 0.0f
+ val v = packedValue and DualUnsignedFloatMask
+ return ((v - 0x00000001_00000001L) and v.inv() and 0x80000000_80000000UL.toLong()) != 0L
+ }
+
+ /** Whether this corner radius describes a quarter circle (x == y). */
+ @Stable
+ inline fun isCircular(): Boolean {
+ return (packedValue ushr 32) == (packedValue and 0xffff_ffffL)
}
/**
@@ -80,7 +95,7 @@
* expressions. For example, negating a radius of one pixel and then adding the result to
* another radius is equivalent to subtracting a radius of one pixel from the other.
*/
- @Stable operator fun unaryMinus() = CornerRadius(-x, -y)
+ @Stable inline operator fun unaryMinus() = CornerRadius(packedValue xor DualFloatSignBit)
/**
* Binary subtraction operator.
@@ -89,7 +104,15 @@
* right-hand-side operand's [x] and whose [y] value is the left-hand-side operand's [y] minus
* the right-hand-side operand's [y].
*/
- @Stable operator fun minus(other: CornerRadius) = CornerRadius(x - other.x, y - other.y)
+ @Stable
+ operator fun minus(other: CornerRadius): CornerRadius {
+ return CornerRadius(
+ packFloats(
+ unpackFloat1(packedValue) - unpackFloat1(other.packedValue),
+ unpackFloat2(packedValue) - unpackFloat2(other.packedValue)
+ )
+ )
+ }
/**
* Binary addition operator.
@@ -97,7 +120,15 @@
* Returns a radius whose [x] value is the sum of the [x] values of the two operands, and whose
* [y] value is the sum of the [y] values of the two operands.
*/
- @Stable operator fun plus(other: CornerRadius) = CornerRadius(x + other.x, y + other.y)
+ @Stable
+ operator fun plus(other: CornerRadius): CornerRadius {
+ return CornerRadius(
+ packFloats(
+ unpackFloat1(packedValue) + unpackFloat1(other.packedValue),
+ unpackFloat2(packedValue) + unpackFloat2(other.packedValue)
+ )
+ )
+ }
/**
* Multiplication operator.
@@ -105,7 +136,11 @@
* Returns a radius whose coordinates are the coordinates of the left-hand-side operand (a
* radius) multiplied by the scalar right-hand-side operand (a Float).
*/
- @Stable operator fun times(operand: Float) = CornerRadius(x * operand, y * operand)
+ @Stable
+ operator fun times(operand: Float) =
+ CornerRadius(
+ packFloats(unpackFloat1(packedValue) * operand, unpackFloat2(packedValue) * operand)
+ )
/**
* Division operator.
@@ -113,7 +148,11 @@
* Returns a radius whose coordinates are the coordinates of the left-hand-side operand (a
* radius) divided by the scalar right-hand-side operand (a Float).
*/
- @Stable operator fun div(operand: Float) = CornerRadius(x / operand, y / operand)
+ @Stable
+ operator fun div(operand: Float) =
+ CornerRadius(
+ packFloats(unpackFloat1(packedValue) / operand, unpackFloat2(packedValue) / operand)
+ )
override fun toString(): String {
return if (x == y) {
@@ -139,5 +178,10 @@
*/
@Stable
fun lerp(start: CornerRadius, stop: CornerRadius, fraction: Float): CornerRadius {
- return CornerRadius(lerp(start.x, stop.x, fraction), lerp(start.y, stop.y, fraction))
+ return CornerRadius(
+ packFloats(
+ lerp(unpackFloat1(start.packedValue), unpackFloat1(stop.packedValue), fraction),
+ lerp(unpackFloat2(start.packedValue), unpackFloat2(stop.packedValue), fraction)
+ )
+ )
}
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt
index e2b7e68..04dc73f 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt
@@ -346,20 +346,18 @@
/** Whether this rounded rectangle is a simple rectangle with zero corner radii. */
val RoundRect.isRect
get(): Boolean =
- (topLeftCornerRadius.x == 0.0f || topLeftCornerRadius.y == 0.0f) &&
- (topRightCornerRadius.x == 0.0f || topRightCornerRadius.y == 0.0f) &&
- (bottomLeftCornerRadius.x == 0.0f || bottomLeftCornerRadius.y == 0.0f) &&
- (bottomRightCornerRadius.x == 0.0f || bottomRightCornerRadius.y == 0.0f)
+ topLeftCornerRadius.isZero() &&
+ topRightCornerRadius.isZero() &&
+ bottomLeftCornerRadius.isZero() &&
+ bottomRightCornerRadius.isZero()
/** Whether this rounded rectangle has no side with a straight section. */
val RoundRect.isEllipse
get(): Boolean =
- topLeftCornerRadius.x == topRightCornerRadius.x &&
- topLeftCornerRadius.y == topRightCornerRadius.y &&
- topRightCornerRadius.x == bottomRightCornerRadius.x &&
- topRightCornerRadius.y == bottomRightCornerRadius.y &&
- bottomRightCornerRadius.x == bottomLeftCornerRadius.x &&
- bottomRightCornerRadius.y == bottomLeftCornerRadius.y &&
+ topLeftCornerRadius.isCircular() &&
+ topRightCornerRadius.isCircular() &&
+ bottomLeftCornerRadius.isCircular() &&
+ bottomRightCornerRadius.isCircular() &&
width <= 2.0 * topLeftCornerRadius.x &&
height <= 2.0 * topLeftCornerRadius.y
@@ -390,13 +388,10 @@
*/
val RoundRect.isSimple: Boolean
get() =
- topLeftCornerRadius.x == topLeftCornerRadius.y &&
- topLeftCornerRadius.x == topRightCornerRadius.x &&
- topLeftCornerRadius.x == topRightCornerRadius.y &&
- topLeftCornerRadius.x == bottomRightCornerRadius.x &&
- topLeftCornerRadius.x == bottomRightCornerRadius.y &&
- topLeftCornerRadius.x == bottomLeftCornerRadius.x &&
- topLeftCornerRadius.x == bottomLeftCornerRadius.y
+ topLeftCornerRadius.isCircular() &&
+ topLeftCornerRadius.packedValue == topRightCornerRadius.packedValue &&
+ topLeftCornerRadius.packedValue == bottomRightCornerRadius.packedValue &&
+ topLeftCornerRadius.packedValue == bottomLeftCornerRadius.packedValue
/**
* Linearly interpolate between two rounded rectangles.
diff --git a/compose/ui/ui-unit/api/current.txt b/compose/ui/ui-unit/api/current.txt
index b0debd1..f22b337 100644
--- a/compose/ui/ui-unit/api/current.txt
+++ b/compose/ui/ui-unit/api/current.txt
@@ -8,6 +8,7 @@
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class Constraints {
ctor public Constraints(@kotlin.PublishedApi long value);
method public long copy(optional int minWidth, optional int maxWidth, optional int minHeight, optional int maxHeight);
+ method public inline long copyMaxDimensions();
method public boolean getHasBoundedHeight();
method public boolean getHasBoundedWidth();
method public boolean getHasFixedHeight();
diff --git a/compose/ui/ui-unit/api/restricted_current.txt b/compose/ui/ui-unit/api/restricted_current.txt
index 4fc0dc1..4512aac 100644
--- a/compose/ui/ui-unit/api/restricted_current.txt
+++ b/compose/ui/ui-unit/api/restricted_current.txt
@@ -8,6 +8,7 @@
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class Constraints {
ctor public Constraints(@kotlin.PublishedApi long value);
method public long copy(optional int minWidth, optional int maxWidth, optional int minHeight, optional int maxHeight);
+ method public inline long copyMaxDimensions();
method public boolean getHasBoundedHeight();
method public boolean getHasBoundedWidth();
method public boolean getHasFixedHeight();
@@ -47,6 +48,7 @@
method @androidx.compose.runtime.Stable public static int constrainWidth(long, int width);
method @androidx.compose.runtime.Stable public static boolean isSatisfiedBy(long, long size);
method @androidx.compose.runtime.Stable public static long offset(long, optional int horizontal, optional int vertical);
+ field @kotlin.PublishedApi internal static final long MaxDimensionsAndFocusMask = -8589934589L; // 0xfffffffe00000003L
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface Density extends androidx.compose.ui.unit.FontScaling {
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
index 06d6e36..e1b4315 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
@@ -196,6 +196,12 @@
return createConstraints(minWidth, maxWidth, minHeight, maxHeight)
}
+ /**
+ * Copies the existing [Constraints], setting [minWidth] and [minHeight] to 0, and preserving
+ * [maxWidth] and [maxHeight] as-is.
+ */
+ inline fun copyMaxDimensions() = Constraints(value and MaxDimensionsAndFocusMask)
+
override fun toString(): String {
val maxWidth = maxWidth
val maxWidthStr = if (maxWidth == Infinity) "Infinity" else maxWidth.toString()
@@ -401,6 +407,9 @@
private const val MaxNonFocusBits = 13
private const val MaxAllowedForMaxNonFocusBits = (1 shl (31 - MaxNonFocusBits)) - 2
+// 0xFFFFFFFE_00000003UL.toLong(), written as a signed value to declare it const
+@PublishedApi internal const val MaxDimensionsAndFocusMask = -0x00000001_FFFFFFFDL
+
// Wrap those throws in functions to avoid inlining the string building at the call sites
// Keep internal for codegen
internal fun throwInvalidConstraintException(widthVal: Int, heightVal: Int) {
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 738fb68..3bbc9dd 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -3516,10 +3516,11 @@
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getError();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getFocused();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getHeading();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getHideFromAccessibility();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.ScrollAxisRange> getHorizontalScrollAxisRange();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.input.ImeAction> getImeAction();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Integer>> getIndexForKey();
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getInvisibleToUser();
+ method @Deprecated public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getInvisibleToUser();
method @Deprecated public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getIsContainer();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsDialog();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getIsEditable();
@@ -3551,10 +3552,11 @@
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> Error;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Focused;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Heading;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> HideFromAccessibility;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.ScrollAxisRange> HorizontalScrollAxisRange;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.input.ImeAction> ImeAction;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Integer>> IndexForKey;
- property @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> InvisibleToUser;
+ property @Deprecated public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> InvisibleToUser;
property @Deprecated public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> IsContainer;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsDialog;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> IsEditable;
@@ -3622,9 +3624,10 @@
method public static float getTraversalIndex(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.semantics.ScrollAxisRange getVerticalScrollAxisRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void heading(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static void hideFromAccessibility(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void indexForKey(androidx.compose.ui.semantics.SemanticsPropertyReceiver, kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Integer> mapping);
method public static void insertTextAtCursor(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString,java.lang.Boolean>? action);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static void invisibleToUser(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method @Deprecated public static void invisibleToUser(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method @Deprecated public static boolean isContainer(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static boolean isEditable(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static boolean isShowingTextSubstitution(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 41d810c..195d272 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -3576,10 +3576,11 @@
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getError();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getFocused();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getHeading();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getHideFromAccessibility();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.ScrollAxisRange> getHorizontalScrollAxisRange();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.input.ImeAction> getImeAction();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Integer>> getIndexForKey();
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getInvisibleToUser();
+ method @Deprecated public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getInvisibleToUser();
method @Deprecated public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getIsContainer();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsDialog();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getIsEditable();
@@ -3611,10 +3612,11 @@
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> Error;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Focused;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Heading;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> HideFromAccessibility;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.ScrollAxisRange> HorizontalScrollAxisRange;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.input.ImeAction> ImeAction;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Integer>> IndexForKey;
- property @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> InvisibleToUser;
+ property @Deprecated public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> InvisibleToUser;
property @Deprecated public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> IsContainer;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsDialog;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> IsEditable;
@@ -3682,9 +3684,10 @@
method public static float getTraversalIndex(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.semantics.ScrollAxisRange getVerticalScrollAxisRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void heading(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static void hideFromAccessibility(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void indexForKey(androidx.compose.ui.semantics.SemanticsPropertyReceiver, kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Integer> mapping);
method public static void insertTextAtCursor(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString,java.lang.Boolean>? action);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static void invisibleToUser(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method @Deprecated public static void invisibleToUser(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method @Deprecated public static boolean isContainer(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static boolean isEditable(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static boolean isShowingTextSubstitution(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
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 11e489d..beb16d4e 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
@@ -143,7 +143,7 @@
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.getOrNull
-import androidx.compose.ui.semantics.invisibleToUser
+import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.paneTitle
import androidx.compose.ui.semantics.role
@@ -3358,10 +3358,15 @@
@OptIn(ExperimentalComposeUiApi::class)
@Test
- fun testSemanticsHitTest_invisibleToUserSemantics() {
+ fun testSemanticsHitTest_hideFromAccessibilitySemantics() {
// Arrange.
setContent {
- Box(Modifier.size(100.dp).clickable {}.testTag(tag).semantics { invisibleToUser() }) {
+ Box(
+ Modifier.size(100.dp)
+ .clickable {}
+ .testTag(tag)
+ .semantics { hideFromAccessibility() }
+ ) {
BasicText("")
}
}
@@ -4106,9 +4111,8 @@
rule.runOnIdle { assertThat(hitNodeId).isEqualTo(InvalidId) }
}
- @OptIn(ExperimentalComposeUiApi::class)
@Test
- fun testAccessibilityNodeInfoTreePruned_invisibleDoesNotPrune() {
+ fun testAccessibilityNodeInfoTreePruned_hideFromAccessibilityDoesNotPrune() {
// Arrange.
val parentTag = "ParentForOverlappedChildren"
val childOneTag = "OverlappedChildOne"
@@ -4120,7 +4124,7 @@
"Child One",
Modifier.zIndex(1f)
.testTag(childOneTag)
- .semantics { invisibleToUser() }
+ .semantics { hideFromAccessibility() }
.requiredSize(50.toDp())
)
BasicText("Child Two", Modifier.testTag(childTwoTag).requiredSize(50.toDp()))
@@ -5321,7 +5325,7 @@
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
layout(layoutWidth, layoutHeight) {
val placeablesOne =
@@ -5352,7 +5356,7 @@
SubcomposeLayout(modifier) { constraints ->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
layout(layoutWidth, layoutHeight) {
val topPlaceables =
subcompose(ScaffoldedSlots.Top, topBar).fastMap { it.measure(looseConstraints) }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 9772f19..7b77f3c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -59,8 +59,8 @@
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.getTextLayoutResult
import androidx.compose.ui.semantics.heading
+import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.horizontalScrollAxisRange
-import androidx.compose.ui.semantics.invisibleToUser
import androidx.compose.ui.semantics.isEditable
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.maxTextLength
@@ -447,7 +447,6 @@
rule.runOnIdle { assertThat(info.isImportantForAccessibility).isTrue() }
}
- @OptIn(ExperimentalComposeUiApi::class)
@Test
@SdkSuppress(minSdkVersion = 24)
fun testIsNotImportant_testOnlyProperties() {
@@ -457,7 +456,7 @@
Modifier.size(10.dp).semantics(mergeDescendants = false) {
testTag = tag
testTagsAsResourceId = true
- invisibleToUser()
+ hideFromAccessibility()
}
)
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index b23a9ac..83b0bf4 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -4212,7 +4212,7 @@
measurable: Measurable,
constraints: Constraints
): MeasureResult {
- val placeable = measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
+ val placeable = measurable.measure(constraints.copyMaxDimensions())
return layout(constraints.maxWidth, constraints.maxHeight) {
placeable.placeRelative(0, 0)
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidSemanticAutofillTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidSemanticAutofillTest.kt
index 0bcff51..0a615a9 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidSemanticAutofillTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidSemanticAutofillTest.kt
@@ -50,6 +50,7 @@
import androidx.compose.ui.semantics.contentDataType
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.contentType
+import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.maxTextLength
import androidx.compose.ui.semantics.onAutofillText
import androidx.compose.ui.semantics.onLongClick
@@ -443,6 +444,45 @@
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
+ fun populateViewStructure_hideFromAccessibility() {
+ // Arrange.
+ val viewStructure: ViewStructure = FakeViewStructure()
+
+ rule.setContentWithAutofillEnabled {
+ Box(
+ Modifier.semantics {
+ contentType = ContentType.Username
+ hideFromAccessibility()
+ }
+ .size(width, height)
+ .testTag(contentTag)
+ )
+ }
+
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert that even if a component is unimportant for accessibility, it can still be
+ // accessed by autofill.
+ Truth.assertThat(viewStructure)
+ .isEqualTo(
+ FakeViewStructure().apply {
+ children.add(
+ FakeViewStructure {
+ virtualId = contentTag.semanticsId()
+ setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
+ setVisibility(View.VISIBLE)
+ }
+ )
+ }
+ )
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_invisibility() {
// Arrange.
val viewStructure: ViewStructure = FakeViewStructure()
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
index 7fda49e..08b702e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
@@ -18,7 +18,6 @@
import android.view.View
import androidx.compose.foundation.layout.Box
-import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusStateImpl.Active
@@ -30,13 +29,8 @@
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalInputModeManager
import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsFocused
-import androidx.compose.ui.test.assertIsNotFocused
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.requestFocus
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
@@ -345,25 +339,6 @@
}
}
- @Test
- fun clearFocus_textFieldLosesFocus() {
- // Arrange.
- val textField = "textField"
- rule.setTestContent(extraItemForInitialFocus = false) {
- TextField(value = "", onValueChange = {}, modifier = Modifier.testTag(textField))
- }
- rule.onNodeWithTag(textField).requestFocus()
-
- // Act.
- rule.runOnIdle { focusManager.clearFocus() }
-
- // Assert.
- when (inputModeManager.inputMode) {
- Keyboard -> rule.onNodeWithTag(textField).assertIsFocused()
- Touch -> rule.onNodeWithTag(textField).assertIsNotFocused()
- }
- }
-
private val FocusManager.rootFocusState: FocusState
get() = (this as FocusOwnerImpl).rootFocusNode.focusState
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt
index d4aebb8..6569265 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.focus
+import android.os.Build
import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
@@ -163,8 +164,15 @@
assertThat(view.isFocused).isTrue()
}
Touch -> {
- assertThat(root.focusOwner.rootState).isEqualTo(Inactive)
- assertThat(view.isFocused).isFalse()
+ // On devices pre-P, clearFocus() will cause a subsequent requestFocus()
+ // the causes another request for focus on the ComposeView.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ assertThat(root.focusOwner.rootState).isEqualTo(ActiveParent)
+ assertThat(view.isFocused).isTrue()
+ } else {
+ assertThat(root.focusOwner.rootState).isEqualTo(Inactive)
+ assertThat(view.isFocused).isFalse()
+ }
}
else -> error("invalid input mode")
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/LayerTouchTransformTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/LayerTouchTransformTest.kt
index d34a969..5edd19e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/LayerTouchTransformTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/LayerTouchTransformTest.kt
@@ -145,7 +145,7 @@
@Composable
fun SimpleLayout(modifier: Modifier, content: @Composable () -> Unit = {}) {
Layout(content, modifier) { measurables, constraints ->
- val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val childConstraints = constraints.copyMaxDimensions()
val placeables = measurables.map { it.measure(childConstraints) }
var containerWidth = constraints.minWidth
var containerHeight = constraints.minHeight
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
index 33cc234..65ca269 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
@@ -135,8 +135,7 @@
}
) { measurables, constraints ->
assertEquals(1, measurables.size)
- val placeable =
- measurables.first().measure(constraints.copy(minWidth = 0, minHeight = 0))
+ val placeable = measurables.first().measure(constraints.copyMaxDimensions())
assertEquals(placeable.width, expectedSize)
assertEquals(placeable.height, expectedSize)
layout(constraints.maxWidth, constraints.maxHeight) { placeable.place(0, 0) }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTest.kt
index e055b23..8ce8418 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTest.kt
@@ -26,8 +26,8 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onPlaced
@@ -36,8 +36,8 @@
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalScrollCaptureInProgress
import androidx.compose.ui.semantics.ScrollAxisRange
+import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.horizontalScrollAxisRange
-import androidx.compose.ui.semantics.invisibleToUser
import androidx.compose.ui.semantics.scrollByOffset
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.verticalScrollAxisRange
@@ -198,12 +198,11 @@
assertThat(target.localVisibleRect).isEqualTo(Rect(0, 0, 10, 10))
}
- @OptIn(ExperimentalComposeUiApi::class)
@Test
- fun search_doesNotFindTarget_whenInvisibleToUser() =
+ fun search_doesNotFindTarget_whenHidingFromAccessibility() =
captureTester.runTest {
captureTester.setContent {
- TestVerticalScrollable(Modifier.semantics { invisibleToUser() })
+ TestVerticalScrollable(Modifier.semantics { hideFromAccessibility() })
}
val targets = captureTester.findCaptureTargets()
@@ -211,6 +210,15 @@
}
@Test
+ fun search_doesNotFindTarget_whenTransparent() =
+ captureTester.runTest {
+ captureTester.setContent { TestVerticalScrollable(Modifier.alpha(0f)) }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).isEmpty()
+ }
+
+ @Test
fun search_doesNotFindTarget_whenZeroSize() =
captureTester.runTest {
captureTester.setContent { TestVerticalScrollable(Modifier.size(0.dp)) }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 237f438..8e4a41b 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -1254,7 +1254,7 @@
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
- val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val looseConstraints = constraints.copyMaxDimensions()
layout(layoutWidth, layoutHeight) {
val placeablesOne =
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchForwardInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchForwardInteropTest.kt
index a599981..c34238f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchForwardInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchForwardInteropTest.kt
@@ -20,11 +20,16 @@
import android.view.KeyEvent as AndroidKeyEvent
import android.view.KeyEvent.ACTION_DOWN
import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.EditText
import android.widget.LinearLayout
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.size
+import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
@@ -631,6 +636,46 @@
rule.onNodeWithTag(composable).assertIsNotFocused()
}
+ @Test
+ fun focusableInTouchMode() {
+ val tag = "tag"
+ lateinit var editText: EditText
+ lateinit var composeView: ComposeView
+ setContent {
+ AndroidView(
+ {
+ LinearLayout(it).also { linearLayout ->
+ linearLayout.orientation = LinearLayout.VERTICAL
+ linearLayout.addView(EditText(linearLayout.context))
+ editText = EditText(linearLayout.context)
+ editText.setSingleLine()
+ editText.setText("1")
+ editText.inputType = EditorInfo.TYPE_NUMBER_VARIATION_NORMAL
+ editText.imeOptions = EditorInfo.IME_FLAG_NAVIGATE_NEXT
+ linearLayout.addView(editText)
+ composeView =
+ ComposeView(linearLayout.context).apply {
+ setContent {
+ Column { TextField("Hello World", {}, Modifier.testTag(tag)) }
+ }
+ }
+ linearLayout.addView(composeView)
+ }
+ },
+ Modifier.safeContentPadding()
+ )
+ }
+ rule.runOnIdle {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ instrumentation.setInTouchMode(true)
+ editText.requestFocusFromTouch()
+ }
+ rule.waitUntil { rule.runOnUiThread { editText.isFocused } }
+ rule.waitForIdle()
+ rule.runOnIdle { editText.onEditorAction(EditorInfo.IME_ACTION_NEXT) }
+ rule.onNodeWithTag(tag).assertIsFocused()
+ }
+
private fun ComposeContentTestRule.focusSearchForward(waitForIdle: Boolean = true) {
if (waitForIdle) waitForIdle()
if (moveFocusProgrammatically) {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
index 0f6acd4..ed8acb3 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
@@ -97,7 +97,7 @@
height?.roundToPx() ?: Constraints.Infinity
)
)
- val childConstraints = containerConstraints.copy(minWidth = 0, minHeight = 0)
+ val childConstraints = containerConstraints.copyMaxDimensions()
var placeable: Placeable? = null
val containerWidth =
if (containerConstraints.hasFixedWidth) {
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 9961afd..5a2b22d 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
@@ -916,11 +916,6 @@
return super.requestFocus(direction, previouslyFocusedRect)
}
- // When we clear focus on Pre P devices, request focus is called even when we are
- // in touch mode. We fix this by assigning initial focus only in non-touch mode.
- // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
- if (isInTouchMode) return false
-
val focusDirection = toFocusDirection(direction) ?: Enter
return focusOwner.focusSearch(
focusDirection = focusDirection,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 72202ff..5ac1902 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -749,7 +749,7 @@
getInfoStateDescriptionOrNull(node) != null ||
getInfoIsCheckable(node)
- return node.isVisible &&
+ return !node.isHidden &&
(node.unmergedConfig.isMergingSemanticsOfDescendants ||
node.isUnmergedLeafNode && isSpeakingNode)
}
@@ -906,7 +906,7 @@
}
// Mark invisible nodes
- info.isVisibleToUser = semanticsNode.isVisible
+ info.isVisibleToUser = !semanticsNode.isHidden
semanticsNode.unmergedConfig.getOrNull(SemanticsProperties.LiveRegion)?.let {
info.liveRegion =
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 cbd29d8..453fb16 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
@@ -23,7 +23,6 @@
import androidx.collection.MutableIntObjectMap
import androidx.collection.MutableIntSet
import androidx.collection.emptyIntObjectMap
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.node.OwnerScope
import androidx.compose.ui.semantics.Role
@@ -33,6 +32,7 @@
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.SemanticsProperties.HideFromAccessibility
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.util.fastForEach
@@ -128,14 +128,12 @@
}
internal fun SemanticsNode.isImportantForAccessibility() =
- isVisible &&
+ !isHidden &&
(unmergedConfig.isMergingSemanticsOfDescendants ||
unmergedConfig.containsImportantForAccessibility())
-// TODO(347749977): go through and remove experimental tag on `invisible` properties
-@OptIn(ExperimentalComposeUiApi::class)
-internal val SemanticsNode.isVisible: Boolean
- get() = !isTransparent && !unmergedConfig.contains(SemanticsProperties.InvisibleToUser)
+internal val SemanticsNode.isHidden: Boolean
+ get() = isTransparent || unmergedConfig.contains(HideFromAccessibility)
internal val DefaultFakeNodeBounds = Rect(0f, 0f, 10f, 10f)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ScrollCapture.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ScrollCapture.android.kt
index 1be1cb3..829aec92 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ScrollCapture.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ScrollCapture.android.kt
@@ -30,7 +30,7 @@
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.boundsInWindow
-import androidx.compose.ui.platform.isVisible
+import androidx.compose.ui.platform.isHidden
import androidx.compose.ui.semantics.SemanticsActions.ScrollByOffset
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
@@ -130,8 +130,11 @@
onCandidate: (ScrollCaptureCandidate) -> Unit
) {
fromNode.visitDescendants { node ->
- // Invisible/disabled nodes can't be candidates, nor can any of their descendants.
- if (!node.isVisible || Disabled in node.unmergedConfig) {
+ // TODO(mnuzen): Verify `isHidden` is needed here.
+ // See b/354723415 for more details.
+ // Transparent, unimportant for accessibility, and disabled nodes can't be candidates, nor
+ // can any of their descendants.
+ if (node.isHidden || Disabled in node.unmergedConfig) {
return@visitDescendants false
}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/ConstraintsTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/ConstraintsTest.kt
index 0615423..b11ce4e 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/ConstraintsTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/layout/ConstraintsTest.kt
@@ -437,6 +437,12 @@
assertEquals(Constraints.Infinity, constraints4.maxWidth)
}
+ @Test
+ fun testCopyMaxDimensions() {
+ val constraints = Constraints(0x283A7620506CEC0L)
+ assertEquals(constraints.copy(minWidth = 0, minHeight = 0), constraints.copyMaxDimensions())
+ }
+
private fun testConstraints(
minWidth: Int = 0,
maxWidth: Int = Constraints.Infinity,
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 cd3d66b..59d71a9 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
@@ -17,7 +17,6 @@
package androidx.compose.ui.semantics
import androidx.compose.runtime.Immutable
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.autofill.ContentDataType
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.geometry.Offset
@@ -96,15 +95,23 @@
val IsTraversalGroup = SemanticsPropertyKey<Boolean>("IsTraversalGroup")
/** @see SemanticsPropertyReceiver.invisibleToUser */
- @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
- @get:ExperimentalComposeUiApi
- @ExperimentalComposeUiApi
+ @Deprecated(
+ "Use `hideFromAccessibility` instead.",
+ replaceWith = ReplaceWith("HideFromAccessibility")
+ )
val InvisibleToUser =
SemanticsPropertyKey<Unit>(
name = "InvisibleToUser",
mergePolicy = { parentValue, _ -> parentValue }
)
+ /** @see SemanticsPropertyReceiver.hideFromAccessibility */
+ val HideFromAccessibility =
+ SemanticsPropertyKey<Unit>(
+ name = "HideFromAccessibility",
+ mergePolicy = { parentValue, _ -> parentValue }
+ )
+
/** @see SemanticsPropertyReceiver.contentType */
// TODO(b/333102566): make these semantics properties public when Autofill is ready to go live
internal val ContentType =
@@ -872,12 +879,32 @@
* redundant with semantics of their parent, consider [SemanticsModifier.clearAndSetSemantics]
* instead.
*/
-@ExperimentalComposeUiApi
+@Deprecated(
+ "Use `hideFromAccessibility()` instead.",
+ replaceWith = ReplaceWith("hideFromAccessibility()"),
+)
+@Suppress("DEPRECATION")
fun SemanticsPropertyReceiver.invisibleToUser() {
this[SemanticsProperties.InvisibleToUser] = Unit
}
/**
+ * If present, this node is considered hidden from accessibility services.
+ *
+ * For example, if the node is currently occluded by a dark semitransparent pane above it, then for
+ * all practical purposes the node should not be announced to the user. Since the system cannot
+ * automatically determine that, this property can be set to make the screen reader linear
+ * navigation skip over this type of node.
+ *
+ * If looking for a way to clear semantics of small items from the UI tree completely because they
+ * are redundant with semantics of their parent, consider [SemanticsModifier.clearAndSetSemantics]
+ * instead.
+ */
+fun SemanticsPropertyReceiver.hideFromAccessibility() {
+ this[SemanticsProperties.HideFromAccessibility] = Unit
+}
+
+/**
* Content field type information.
*
* This API can be used to indicate to Autofill services what _kind of field_ is associated with
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt
index 630a3e9..387ddd4 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt
@@ -20,6 +20,7 @@
import android.content.Intent
import android.os.Build
import android.os.Build.VERSION_CODES
+import android.util.Log
import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.CallControlResult
import androidx.core.telecom.InCallServiceCompat
@@ -41,6 +42,7 @@
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.rule.ServiceTestRule
+import java.util.concurrent.atomic.AtomicInteger
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
@@ -65,6 +67,7 @@
@RunWith(Parameterized::class)
class E2EExtensionTests(private val parameters: TestParameters) : BaseTelecomTest() {
companion object {
+ private val LOG_TAG = E2EExtensionTests::class.simpleName
private const val ICS_EXTENSION_UPDATE_TIMEOUT_MS = 1000L
// Use the VOIP service that uses V2 APIs (VoipAppExtensionControl)
private const val SERVICE_SOURCE_V2 = 1
@@ -171,6 +174,8 @@
@get:Rule val voipAppServiceRule: ServiceTestRule = ServiceTestRule()
+ private val mRequestIdGenerator = AtomicInteger(0)
+
data class TestParameters(val serviceSource: Int, val direction: Int) {
override fun toString(): String {
return "${directionToString(direction)}-${sourceToString(serviceSource)}"
@@ -209,10 +214,13 @@
fun testVoipWithExtensionsAndInCallServiceWithout() = runBlocking {
usingIcs { ics ->
val voipAppControl = bindToVoipAppWithExtensions()
+ val callback = TestCallCallbackListener(this)
+ voipAppControl.setCallback(callback)
// No Capability Exchange sequence occurs between VoIP app and ICS because ICS doesn't
// support extensions
createAndVerifyVoipCall(
voipAppControl,
+ callback,
listOf(CAPABILITY_PARTICIPANT_WITH_ACTIONS),
parameters.direction
)
@@ -241,6 +249,7 @@
voipAppControl.setCallback(callback)
createAndVerifyVoipCall(
voipAppControl,
+ callback,
listOf(getParticipantCapability(emptySet())),
parameters.direction
)
@@ -279,6 +288,57 @@
}
/**
+ * On some Android versions (U & V), setting up an extension quickly after the ICS receives the
+ * new call can cause the CAPABILITY_EXCHANGE event to drop internally in Telecom.
+ *
+ * Run 10 iterations of adding a new call + setting up extensions to test that we do not hit
+ * this condition.
+ */
+ @LargeTest
+ @Test(timeout = 10000)
+ fun testVoipAndIcsWithParticipantsRace() = runBlocking {
+ usingIcs { ics ->
+ val iterations = 10
+ val voipAppControl = bindToVoipAppWithExtensions()
+ val callback = TestCallCallbackListener(this)
+ voipAppControl.setCallback(callback)
+ val failedTries = ArrayList<Int>()
+ for (i in 1..iterations) {
+ Log.i(LOG_TAG, "testVoipAndIcsWithParticipantsStress: try#$i")
+ val requestId = mRequestIdGenerator.getAndIncrement()
+ // Only wait for call setup on ICS side to stress extensions setup
+ createVoipCallAsync(
+ voipAppControl,
+ requestId,
+ listOf(getParticipantCapability(emptySet())),
+ parameters.direction
+ )
+ var hasConnected = false
+ with(ics) {
+ val call = TestUtils.waitOnInCallServiceToReachXCalls(ics, 1)!!
+ connectExtensions(call) {
+ val participants = CachedParticipants(this)
+ onConnected {
+ hasConnected = true
+ if (!participants.extension.isSupported) {
+ failedTries.add(i)
+ }
+ call.disconnect()
+ }
+ }
+ }
+ assertTrue("onConnected never received", hasConnected)
+ // Ensure the ICS mCalls list is updated with the newly removed call so we don't
+ // accidentally grab the stale call when starting the next round.
+ TestUtils.waitOnInCallServiceToReachXCalls(ics, 0)
+ }
+ if (failedTries.isNotEmpty()) {
+ fail("Failed to set up extensions on ${failedTries.size}/$iterations tries")
+ }
+ }
+ }
+
+ /**
* Create a VOIP call with a participants extension and attach participant Call extensions.
* Verify raised hands functionality works as expected
*/
@@ -292,6 +352,7 @@
val voipCallId =
createAndVerifyVoipCall(
voipAppControl,
+ callback,
listOf(
getParticipantCapability(setOf(ParticipantExtensionImpl.RAISE_HAND_ACTION))
),
@@ -353,6 +414,7 @@
val voipCallId =
createAndVerifyVoipCall(
voipAppControl,
+ callback,
listOf(getLocalSilenceCapability(setOf())),
parameters.direction
)
@@ -397,6 +459,7 @@
val voipCallId =
createAndVerifyVoipCall(
voipAppControl,
+ callback,
listOf(
getParticipantCapability(
setOf(ParticipantExtensionImpl.KICK_PARTICIPANT_ACTION)
@@ -439,24 +502,35 @@
* Helpers
* =========================================================================================
*/
+ private fun createVoipCallAsync(
+ voipAppControl: ITestAppControl,
+ requestId: Int,
+ capabilities: List<Capability>,
+ direction: Int
+ ) {
+ // add a call to verify capability exchange IS made with ICS
+ voipAppControl.addCall(
+ requestId,
+ capabilities,
+ direction == CallAttributesCompat.DIRECTION_OUTGOING
+ )
+ }
/**
* Creates a VOIP call using the specified capabilities and direction and then verifies that it
* was set up.
*/
- private fun createAndVerifyVoipCall(
+ private suspend fun createAndVerifyVoipCall(
voipAppControl: ITestAppControl,
+ callback: TestCallCallbackListener,
capabilities: List<Capability>,
direction: Int
): String {
- // add a call to verify capability exchange IS made with ICS
- val voipCallId =
- voipAppControl.addCall(
- capabilities,
- direction == CallAttributesCompat.DIRECTION_OUTGOING
- )
- assertTrue("call could not be created", voipCallId.isNotEmpty())
- return voipCallId
+ val requestId = mRequestIdGenerator.getAndIncrement()
+ createVoipCallAsync(voipAppControl, requestId, capabilities, direction)
+ val callId = callback.waitForCallAdded(requestId)
+ assertTrue("call could not be created", !callId.isNullOrEmpty())
+ return callId!!
}
/** Sets up the test based on the parameters set for the run */
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt
index aeea2a7..74aca2a 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt
@@ -37,10 +37,8 @@
import androidx.core.telecom.test.utils.TestUtils
import androidx.core.telecom.util.ExperimentalAppActions
import kotlin.coroutines.cancellation.CancellationException
-import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop
@@ -49,7 +47,7 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
-@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAppActions::class)
+@OptIn(ExperimentalAppActions::class)
@RequiresApi(Build.VERSION_CODES.O)
open class VoipAppWithExtensionsControl : Service() {
var mCallsManager: CallsManager? = null
@@ -87,10 +85,13 @@
Log.i(TAG, "onDisconnect: disconnectCause=[$it]")
}
- override fun addCall(capabilities: List<Capability>, isOutgoing: Boolean): String {
- var id = ""
+ override fun addCall(
+ requestId: Int,
+ capabilities: List<Capability>,
+ isOutgoing: Boolean
+ ) {
+ Log.i(TAG, "VoipAppWithExtensionsControl: addCall: request")
runBlocking {
- val deferredId = CompletableDeferred<String>()
val call = VoipCall(mCallsManager!!, mCallback, capabilities)
mScope?.launch {
with(call) {
@@ -132,14 +133,11 @@
localCallSilenceUpdater?.updateIsLocallySilenced(it)
}
.launchIn(this)
- deferredId.complete(this.getCallId().toString())
+ mCallback?.onCallAdded(requestId, this.getCallId().toString())
}
}
}
- deferredId.await()
- id = deferredId.getCompleted()
}
- return id
}
override fun updateParticipants(setOfParticipants: List<ParticipantParcelable>) {
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestInCallService.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestInCallService.kt
index 6e23441..b9e6b5c 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestInCallService.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestInCallService.kt
@@ -24,7 +24,6 @@
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.telecom.InCallServiceCompat
-import androidx.core.telecom.util.ExperimentalAppActions
import java.util.Collections
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
@@ -116,7 +115,6 @@
mCalls.clear()
}
- @ExperimentalAppActions
fun getLastCall(): Call? {
return if (mCalls.size == 0) {
null
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
index 214a196..676c404 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
@@ -47,6 +47,7 @@
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
@@ -59,29 +60,30 @@
object TestUtils {
const val LOG_TAG = "TelecomTestUtils"
const val TEST_PACKAGE = "androidx.core.telecom.test"
- const val COMMAND_SET_DEFAULT_DIALER = "telecom set-default-dialer " // DO NOT REMOVE SPACE
- const val COMMAND_GET_DEFAULT_DIALER = "telecom get-default-dialer"
- const val COMMAND_ENABLE_PHONE_ACCOUNT = "telecom set-phone-account-enabled "
+ private const val COMMAND_SET_DEFAULT_DIALER =
+ "telecom set-default-dialer " // DO NOT REMOVE SPACE
+ private const val COMMAND_GET_DEFAULT_DIALER = "telecom get-default-dialer"
+ private const val COMMAND_ENABLE_PHONE_ACCOUNT = "telecom set-phone-account-enabled "
const val COMMAND_CLEANUP_STUCK_CALLS = "telecom cleanup-stuck-calls"
const val COMMAND_DUMP_TELECOM = "dumpsys telecom"
const val TEST_CALL_ATTRIB_NAME = "Elon Musk"
const val OUTGOING_NAME = "Larry Page"
- const val INCOMING_NAME = "Sundar Pichai"
+ private const val INCOMING_NAME = "Sundar Pichai"
const val WAIT_ON_ASSERTS_TO_FINISH_TIMEOUT = 10000L
const val WAIT_ON_CALL_STATE_TIMEOUT = 8000L
- const val WAIT_ON_IN_CALL_SERVICE_CALL_COUNT_TIMEOUT = 5000L
- const val WAIT_ON_IN_CALL_SERVICE_CALL_COMPAT_COUNT_TIMEOUT = 5000L
+ private const val WAIT_ON_IN_CALL_SERVICE_CALL_COUNT_TIMEOUT = 5000L
const val ALL_CALL_CAPABILITIES =
(CallAttributesCompat.SUPPORTS_SET_INACTIVE or
CallAttributesCompat.SUPPORTS_STREAM or
CallAttributesCompat.SUPPORTS_TRANSFER)
- val VERIFICATION_TIMEOUT_MSG =
+ const val VERIFICATION_TIMEOUT_MSG =
"Timed out before asserting all values. This most likely means the platform failed to" +
" add the call or hung on a CallControl operation."
- val CALLBACK_FAILED_EXCEPTION_MSG = "callback failed to be completed in the lambda function"
+ private const val CALLBACK_FAILED_EXCEPTION_MSG =
+ "callback failed to be completed in the lambda function"
// non-primitive constants
- val TEST_PHONE_NUMBER_9001 = Uri.parse("tel:6506959001")
- val TEST_PHONE_NUMBER_8985 = Uri.parse("tel:6506958985")
+ val TEST_PHONE_NUMBER_9001: Uri = Uri.parse("tel:6506959001")
+ val TEST_PHONE_NUMBER_8985: Uri = Uri.parse("tel:6506958985")
// Define the minimal set of properties to start an outgoing call
val OUTGOING_CALL_ATTRIBUTES =
@@ -298,78 +300,73 @@
return ParcelUuid.fromString(UUID.randomUUID().toString())
}
- @OptIn(ExperimentalAppActions::class)
- @Suppress("deprecation")
+ /**
+ * Suspends until the [targetCallCount] is reached, or times out after
+ * [WAIT_ON_IN_CALL_SERVICE_CALL_COUNT_TIMEOUT] milliseconds.
+ */
internal suspend fun waitOnInCallServiceToReachXCalls(
service: TestInCallService,
targetCallCount: Int
): Call? {
- var targetCall: Call?
- try {
- withTimeout(WAIT_ON_IN_CALL_SERVICE_CALL_COUNT_TIMEOUT) {
- Log.i(LOG_TAG, "waitOnInCallServiceToReachXCalls: starting call check")
- while (isActive && (service.getCallCount() < targetCallCount)) {
- yield() // ensure the coroutine is not canceled
- delay(1) // sleep x millisecond(s) instead of spamming check
- }
- targetCall = service.getLastCall()
- Log.i(LOG_TAG, "waitOnInCallServiceToReachXCalls: found targetCall=[$targetCall]")
- }
- } catch (e: TimeoutCancellationException) {
- Log.i(LOG_TAG, "waitOnInCallServiceToReachXCalls: timeout reached")
- dumpTelecom()
- service.destroyAllCalls()
- throw AssertionError(
+ var targetCall: Call? = null
+ Log.i(
+ LOG_TAG,
+ "waitOnInCallServiceToReachXCalls: target count=$targetCallCount, " +
+ "starting call check"
+ )
+ if (targetCallCount > 0) {
+ waitForCondition(
+ WAIT_ON_IN_CALL_SERVICE_CALL_COUNT_TIMEOUT,
"Expected call count to be <$targetCallCount>" +
" but the Actual call count was <${service.getCallCount()}>"
- )
+ ) {
+ service.getCallCount() >= targetCallCount
+ }
+ targetCall = service.getLastCall()
+ Log.i(LOG_TAG, "waitOnInCallServiceToReachXCalls: found targetCall=[$targetCall]")
+ } else {
+ waitForCondition(
+ WAIT_ON_IN_CALL_SERVICE_CALL_COUNT_TIMEOUT,
+ "Expected call count to be <$targetCallCount>" +
+ " but the Actual call count was <${service.getCallCount()}>"
+ ) {
+ service.getCallCount() <= 0
+ }
+ Log.i(LOG_TAG, "waitOnInCallServiceToReachXCalls: reached 0 calls")
}
return targetCall
}
- @Suppress("deprecation")
+ @Suppress("DEPRECATION")
suspend fun waitOnCallState(call: Call, targetState: Int) {
+ waitForCondition(
+ WAIT_ON_CALL_STATE_TIMEOUT,
+ "Expected call state to be <$targetState>" +
+ " but the Actual call state was <${call.state}>"
+ ) {
+ call.state == targetState
+ }
+ }
+
+ private suspend fun waitForCondition(
+ timeout: Long,
+ failureMessage: String,
+ expectedCondition: () -> Boolean
+ ) {
try {
- withTimeout(WAIT_ON_CALL_STATE_TIMEOUT) {
- while (isActive /* aka within timeout window */ && (call.state != targetState)) {
+ withTimeout(timeout) {
+ while (isActive /* aka within timeout window */ && !expectedCondition()) {
yield() // another mechanism to stop the while loop if the coroutine is dead
delay(1) // sleep x millisecond(s) instead of spamming check
}
}
} catch (e: TimeoutCancellationException) {
- Log.i(LOG_TAG, "waitOnCallState: timeout reached")
+ Log.i(LOG_TAG, "waitOnCondition: timeout reached")
dumpTelecom()
- throw AssertionError(
- "Expected call state to be <$targetState>" +
- " but the Actual call state was <${call.state}>"
- )
+ throw AssertionError(failureMessage)
}
}
- /** Helper to wait on the call detail extras to be populated from the connection service */
- suspend fun waitOnCallExtras(call: Call) {
- try {
- withTimeout(WAIT_ON_CALL_STATE_TIMEOUT) {
- while (isActive /* aka within timeout window */ && isCallDetailExtrasEmpty(call)) {
- yield() // another mechanism to stop the while loop if the coroutine is dead
- delay(1) // sleep x millisecond(s) instead of spamming check
- }
- }
- } catch (e: TimeoutCancellationException) {
- Log.i(LOG_TAG, "waitOnCallExtras: timeout reached")
- dumpTelecom()
- throw AssertionError("Expected call detail extras to be non-null.")
- }
- }
-
- /**
- * Helper used to determine if the call detail extras is empty or null, which is used as a basis
- * for waiting in the voip app action tests (around capability exchange).
- */
- private fun isCallDetailExtrasEmpty(call: Call): Boolean {
- return call.details?.extras == null || call.details.extras.isEmpty
- }
-
/**
* Used for testing in V. The build version is not available for referencing so this helper
* performs a manual check instead.
@@ -379,11 +376,6 @@
return Build.VERSION.SDK_INT > 34
}
- /** Determine if the current build supports at least U. */
- fun buildIsAtLeastU(): Boolean {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
- }
-
/** Generate a List of [Participant]s, where each ID corresponds to a range of 1 to [num] */
@ExperimentalAppActions
fun generateParticipants(num: Int): List<Participant> {
@@ -421,6 +413,12 @@
MutableSharedFlow(replay = 1)
private val isLocallySilencedFlow: MutableSharedFlow<Pair<String, Boolean>> =
MutableStateFlow(Pair("", false))
+ private val callAddedFlow: MutableSharedFlow<Pair<Int, String>> = MutableSharedFlow(replay = 1)
+
+ override fun onCallAdded(requestId: Int, callId: String?) {
+ if (callId == null) return
+ scope.launch { callAddedFlow.emit(Pair(requestId, callId)) }
+ }
override fun raiseHandStateAction(callId: String?, isHandRaised: Boolean) {
if (callId == null) return
@@ -437,6 +435,12 @@
scope.launch { isLocallySilencedFlow.emit(Pair(callId, isLocallySilenced)) }
}
+ suspend fun waitForCallAdded(requestId: Int): String? {
+ return withTimeoutOrNull(5000) {
+ callAddedFlow.filter { it.first == requestId }.map { it.second }.first()
+ }
+ }
+
suspend fun waitForRaiseHandState(callId: String, expectedState: Boolean) {
val result =
withTimeoutOrNull(5000) {
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl
index 455ec11..3850248 100644
--- a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl
@@ -7,9 +7,9 @@
// NOTE: only supports one voip call at a time right now + suspend functions are not supported by
// AIDL :(
@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
-interface ITestAppControl {
+oneway interface ITestAppControl {
void setCallback(in ITestAppControlCallback callback);
- String addCall(in List<Capability> capabilities, boolean isOutgoing);
+ void addCall(in int requestId, in List<Capability> capabilities, boolean isOutgoing);
void updateParticipants(in List<ParticipantParcelable> participants);
void updateActiveParticipant(in ParticipantParcelable participant);
void updateRaisedHands(in List<ParticipantParcelable> raisedHandsParticipants);
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl
index 8e88d62..e508c83 100644
--- a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl
@@ -4,6 +4,7 @@
@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
oneway interface ITestAppControlCallback {
+ void onCallAdded(int requestId, in String callId);
void raiseHandStateAction(in String callId, boolean isHandRaised);
void kickParticipantAction(in String callId, in ParticipantParcelable participant);
void setLocalCallSilenceState(in String callId, boolean isLocallySilenced);
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
index e367139..e80acec 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
@@ -118,6 +118,12 @@
"android.telecom.extra.VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED"
/**
+ * Event sent from the call producer application to the external call surfaces to notify
+ * them that the call has been successfully setup and is ready to be used.
+ */
+ internal const val EVENT_CALL_READY = "androidx.core.telecom.EVENT_CALL_READY"
+
+ /**
* The connection is using transactional call APIs.
*
* The underlying connection was added as a transactional call via the
@@ -503,6 +509,7 @@
coroutineContext
)
+ callSession.sendEvent(EVENT_CALL_READY)
callSession.maybeSwitchStartingEndpoint(callAttributes.preferredStartingCallEndpoint)
// Run the clients code with the session active and exposed via the CallControlScope
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScopeImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScopeImpl.kt
index 39674ef..a58d5e2 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScopeImpl.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScopeImpl.kt
@@ -104,6 +104,7 @@
internal const val CAPABILITY_EXCHANGE_VERSION = 1
internal const val RESOLVE_EXTENSIONS_TYPE_TIMEOUT_MS = 1000L
+ internal const val CALL_READY_TIMEOUT_MS = 500L
internal const val CAPABILITY_EXCHANGE_TIMEOUT_MS = 1000L
/** Constants used to denote the extension level supported by the VOIP app. */
@@ -328,6 +329,15 @@
* does not support extensions at all.
*/
private suspend fun performExchangeWithRemote(): CapabilityExchangeResult? {
+ if (Utils.hasPlatformV2Apis()) {
+ Log.d(TAG, "performExchangeWithRemote: waiting for call ready signal...")
+ withTimeoutOrNull(CALL_READY_TIMEOUT_MS) {
+ // On Android U/V, we must wait for the jetpack lib to send a call ready event to
+ // prevent a race between telecom setting the TransactionalServiceWrapper and
+ // sending the CAPABILITY_EXCHANGE event
+ waitForCallReady()
+ }
+ }
Log.d(TAG, "performExchangeWithRemote: requesting extensions from remote")
val extensions =
withTimeoutOrNull(CAPABILITY_EXCHANGE_TIMEOUT_MS) { registerWithRemoteService() }
@@ -337,6 +347,21 @@
return extensions
}
+ /** Wait for the Call to receive [CallsManager.EVENT_CALL_READY] from the call producer. */
+ private suspend fun waitForCallReady() = suspendCancellableCoroutine { continuation ->
+ val callback =
+ object : Callback() {
+ override fun onConnectionEvent(call: Call?, event: String?, extras: Bundle?) {
+ if (call == null || event == null) return
+ if (event == CallsManager.EVENT_CALL_READY) {
+ continuation.resume(Unit)
+ }
+ }
+ }
+ call.registerCallback(callback, Handler(Looper.getMainLooper()))
+ continuation.invokeOnCancellation { call.unregisterCallback(callback) }
+ }
+
/**
* Initialize all extensions that were registered with [registerExtension] and provide the
* negotiated capability or null if the remote doesn't support this extension.
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt
index e96ccbd..7808490 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt
@@ -376,6 +376,14 @@
return result.getCompleted()
}
+ fun sendEvent(event: String, extras: Bundle = Bundle.EMPTY) {
+ if (mPlatformInterface == null) {
+ Log.w(TAG, "sendEvent: platform interface is not set up, [$event] dropped")
+ return
+ }
+ mPlatformInterface!!.sendEvent(event, extras)
+ }
+
suspend fun requestEndpointChange(endpoint: CallEndpointCompat): CallControlResult {
val job: CompletableDeferred<CallControlResult> = CompletableDeferred()
// cache the last CallEndpoint the user requested to reference in
diff --git a/fragment/fragment-compose/src/androidTest/java/androidx/fragment/compose/AndroidFragmentTest.kt b/fragment/fragment-compose/src/androidTest/java/androidx/fragment/compose/AndroidFragmentTest.kt
index a1369c7..9f70cc2 100644
--- a/fragment/fragment-compose/src/androidTest/java/androidx/fragment/compose/AndroidFragmentTest.kt
+++ b/fragment/fragment-compose/src/androidTest/java/androidx/fragment/compose/AndroidFragmentTest.kt
@@ -180,6 +180,31 @@
onView(withText("Show me on Screen")).check(matches(isDisplayed()))
}
+
+ @Test
+ fun recomposeWhenSwapFragmentClass() {
+
+ lateinit var clazz: MutableState<Class<out Fragment>>
+ testRule.setContent {
+ clazz = remember { mutableStateOf(FragmentForCompose::class.java) }
+ AndroidFragment(
+ clazz = clazz.value,
+ arguments = bundleOf("name" to clazz.value.simpleName)
+ )
+ }
+
+ testRule.waitForIdle()
+
+ onView(withText("My name is ${FragmentForCompose::class.simpleName}"))
+ .check(matches(isDisplayed()))
+
+ testRule.runOnIdle { clazz.value = FragmentForCompose2::class.java }
+
+ testRule.waitForIdle()
+
+ onView(withText("My name is ${FragmentForCompose2::class.simpleName}"))
+ .check(matches(isDisplayed()))
+ }
}
class FragmentForCompose : Fragment(R.layout.content) {
@@ -191,3 +216,13 @@
}
}
}
+
+class FragmentForCompose2 : Fragment(R.layout.content) {
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val name = arguments?.getString("name")
+ if (name != null) {
+ val textView = view.findViewById<TextView>(R.id.text)
+ textView.text = "My name is $name"
+ }
+ }
+}
diff --git a/fragment/fragment-compose/src/main/java/androidx/fragment/compose/AndroidFragment.kt b/fragment/fragment-compose/src/main/java/androidx/fragment/compose/AndroidFragment.kt
index 7d455e9..1d3a679 100644
--- a/fragment/fragment-compose/src/main/java/androidx/fragment/compose/AndroidFragment.kt
+++ b/fragment/fragment-compose/src/main/java/androidx/fragment/compose/AndroidFragment.kt
@@ -16,7 +16,9 @@
package androidx.fragment.compose
+import android.content.Context
import android.os.Bundle
+import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.currentCompositeKeyHash
@@ -84,20 +86,13 @@
val view = LocalView.current
val fragmentManager = remember(view) { FragmentManager.findFragmentManager(view) }
val context = LocalContext.current
- lateinit var container: FragmentContainerView
- AndroidView(
- {
- container = FragmentContainerView(context)
- container.id = hashKey
- container
- },
- modifier
- )
+ val containerFactory = remember { FragmentContainerViewFactory(hashKey) }
+ AndroidView(factory = containerFactory, modifier)
- DisposableEffect(fragmentManager, clazz, fragmentState) {
+ DisposableEffect(fragmentManager, containerFactory, clazz, fragmentState) {
var removeEvenIfStateIsSaved = false
val fragment =
- fragmentManager.findFragmentById(container.id)
+ fragmentManager.findFragmentById(containerFactory.container.id)
?: fragmentManager.fragmentFactory
.instantiate(context.classLoader, clazz.name)
.apply {
@@ -107,7 +102,7 @@
fragmentManager
.beginTransaction()
.setReorderingAllowed(true)
- .add(container, this, "$hashKey")
+ .add(containerFactory.container, this, "$hashKey")
if (fragmentManager.isStateSaved) {
// If the state is saved when we add the fragment,
// we want to remove the Fragment in onDispose
@@ -128,7 +123,7 @@
transaction.commitNow()
}
}
- fragmentManager.onContainerAvailable(container)
+ fragmentManager.onContainerAvailable(containerFactory.container)
@Suppress("UNCHECKED_CAST") updateCallback.value(fragment as T)
onDispose {
val state = fragmentManager.saveFragmentInstanceState(fragment)
@@ -146,3 +141,23 @@
}
}
}
+
+private class FragmentContainerViewFactory(private val containerId: Int) : (Context) -> View {
+
+ // Backing field that stores the last created container
+ // that is assumed to be created always before it is access
+ // via the container property
+ private var lastCreatedContainer: FragmentContainerView? = null
+
+ val container: FragmentContainerView
+ get() =
+ checkNotNull(lastCreatedContainer) {
+ "AndroidView has not created a container for $containerId yet"
+ }
+
+ override operator fun invoke(context: Context) =
+ FragmentContainerView(context).also { container ->
+ container.id = containerId
+ lastCreatedContainer = container
+ }
+}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
index b1923e8..642f4d3 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
@@ -831,17 +831,39 @@
"Unable to start transition $mergedTransition for container $container."
}
seekCancelLambda = {
- if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
- Log.v(FragmentManager.TAG, "Animating to start")
- }
- transitionImpl.animateToStart(controller!!) {
- transitionInfos.forEach { transitionInfo ->
- val operation = transitionInfo.operation
- val view = operation.fragment.view
- if (view != null) {
- operation.finalState.applyState(view, container)
+ if (transitionInfos.all { it.operation.isSeeking }) {
+ if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+ Log.v(FragmentManager.TAG, "Animating to start")
+ }
+ transitionImpl.animateToStart(controller!!) {
+ transitionInfos.forEach { transitionInfo ->
+ val operation = transitionInfo.operation
+ val view = operation.fragment.view
+ if (view != null) {
+ operation.finalState.applyState(view, container)
+ }
}
}
+ } else {
+ if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+ Log.v(FragmentManager.TAG, "Completing animating immediately")
+ }
+ @Suppress("DEPRECATION")
+ val cancelSignal = androidx.core.os.CancellationSignal()
+ transitionImpl.setListenerForTransitionEnd(
+ transitionInfos[0].operation.fragment,
+ mergedTransition,
+ cancelSignal
+ ) {
+ if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+ Log.v(
+ FragmentManager.TAG,
+ "Transition for all operations has completed"
+ )
+ }
+ transitionInfos.forEach { it.operation.completeEffect(this) }
+ }
+ cancelSignal.cancel()
}
}
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt b/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt
index 55efd95..47f8c64 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/SpecialEffectsController.kt
@@ -207,6 +207,12 @@
synchronized(pendingOperations) {
val currentlyRunningOperations = runningOperations.toMutableList()
runningOperations.clear()
+ // If we have no pendingOperations, we should always cancel without seeking,
+ // otherwise, we should check if the fragment has mTransitioning set.
+ for (operation in currentlyRunningOperations) {
+ operation.isSeeking =
+ pendingOperations.isNotEmpty() && operation.fragment.mTransitioning
+ }
for (operation in currentlyRunningOperations) {
// Another operation is about to run while we already have operations running
// There are 2 cases that need to be handled:
@@ -232,12 +238,7 @@
"SpecialEffectsController: Cancelling operation $operation"
)
}
- // If we have no pendingOperations, we should always cancel without seeking,
- // otherwise, we should check if the fragment has mTransitioning set.
- operation.cancel(
- container,
- pendingOperations.isNotEmpty() && operation.fragment.mTransitioning
- )
+ operation.cancel(container)
}
runningNonSeekableTransition = false
if (!operation.isComplete) {
@@ -339,6 +340,9 @@
// First cancel running operations
val runningOperations = runningOperations.toMutableList()
for (operation in runningOperations) {
+ operation.isSeeking = false
+ }
+ for (operation in runningOperations) {
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
val notAttachedMessage =
if (attachedToWindow) {
@@ -359,6 +363,9 @@
// Then cancel pending operations
val pendingOperations = pendingOperations.toMutableList()
for (operation in pendingOperations) {
+ operation.isSeeking = false
+ }
+ for (operation in pendingOperations) {
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
val notAttachedMessage =
if (attachedToWindow) {
@@ -606,7 +613,7 @@
private set
var isSeeking = false
- private set
+ internal set
var isStarted = false
private set
@@ -638,16 +645,6 @@
}
}
- fun cancel(container: ViewGroup, withSeeking: Boolean) {
- if (isCanceled) {
- return
- }
- if (withSeeking) {
- isSeeking = true
- }
- cancel(container)
- }
-
fun mergeWith(finalState: State, lifecycleImpact: LifecycleImpact) {
when (lifecycleImpact) {
LifecycleImpact.ADDING ->
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 0c19032..bd24a73 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -55,6 +55,7 @@
<trust group="^com[.]android($|([.].*))" version="8.1.0" regex="true" reason="old version, before signing"/>
<trust group="^com[.]android($|([.].*))" version="8.5.0-alpha05" regex="true" reason="old version, before signing"/>
<trust group="androidx[.]annotation" version="1\.9\.0-alpha[0-9][2-9]" regex="true" reason="New versions, not yet signed"/>
+ <trust group="androidx[.]annotation" version="1\.9\.0-beta[0-9][2-9]" regex="true" reason="New versions, not yet signed"/>
<trust group="androidx[.]annotation" version="1\.[0-7]\..*" regex="true" reason="Old versions, before signing"/>
<trust group="androidx[.]collection" version="1\.5\.0-alpha[0-9][1-9]" regex="true" reason="Old versions, before signing"/>
<trust group="androidx[.]collection" version="1\.[0-3]\..*" regex="true" reason="Old versions, before signing"/>
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 131b06a..43c2df3 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -467,6 +467,61 @@
public static final class ElevationGainedRecord.Companion {
}
+ public abstract class ExerciseCompletionGoal {
+ }
+
+ public static final class ExerciseCompletionGoal.ActiveCaloriesBurnedGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.ActiveCaloriesBurnedGoal(androidx.health.connect.client.units.Energy activeCalories);
+ method public androidx.health.connect.client.units.Energy getActiveCalories();
+ property public final androidx.health.connect.client.units.Energy activeCalories;
+ }
+
+ public static final class ExerciseCompletionGoal.DistanceAndDurationGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.DistanceAndDurationGoal(androidx.health.connect.client.units.Length distance, java.time.Duration duration);
+ method public androidx.health.connect.client.units.Length getDistance();
+ method public java.time.Duration getDuration();
+ property public final androidx.health.connect.client.units.Length distance;
+ property public final java.time.Duration duration;
+ }
+
+ public static final class ExerciseCompletionGoal.DistanceGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.DistanceGoal(androidx.health.connect.client.units.Length distance);
+ method public androidx.health.connect.client.units.Length getDistance();
+ property public final androidx.health.connect.client.units.Length distance;
+ }
+
+ public static final class ExerciseCompletionGoal.DurationGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.DurationGoal(java.time.Duration duration);
+ method public java.time.Duration getDuration();
+ property public final java.time.Duration duration;
+ }
+
+ public static final class ExerciseCompletionGoal.ManualCompletion extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ field public static final androidx.health.connect.client.records.ExerciseCompletionGoal.ManualCompletion INSTANCE;
+ }
+
+ public static final class ExerciseCompletionGoal.RepetitionsGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.RepetitionsGoal(java.time.Duration repetitions);
+ method public java.time.Duration getRepetitions();
+ property public final java.time.Duration repetitions;
+ }
+
+ public static final class ExerciseCompletionGoal.StepsGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.StepsGoal(int steps);
+ method public int getSteps();
+ property public final int steps;
+ }
+
+ public static final class ExerciseCompletionGoal.TotalCaloriesBurnedGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.TotalCaloriesBurnedGoal(androidx.health.connect.client.units.Energy totalCalories);
+ method public androidx.health.connect.client.units.Energy getTotalCalories();
+ property public final androidx.health.connect.client.units.Energy totalCalories;
+ }
+
+ public static final class ExerciseCompletionGoal.UnknownGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ field public static final androidx.health.connect.client.records.ExerciseCompletionGoal.UnknownGoal INSTANCE;
+ }
+
public final class ExerciseLap {
ctor public ExerciseLap(java.time.Instant startTime, java.time.Instant endTime, optional androidx.health.connect.client.units.Length? length);
method public java.time.Instant getEndTime();
@@ -477,6 +532,61 @@
property public final java.time.Instant startTime;
}
+ public abstract class ExercisePerformanceTarget {
+ }
+
+ public static final class ExercisePerformanceTarget.AmrapTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ field public static final androidx.health.connect.client.records.ExercisePerformanceTarget.AmrapTarget INSTANCE;
+ }
+
+ public static final class ExercisePerformanceTarget.CadenceTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.CadenceTarget(double minCadence, double maxCadence);
+ method public double getMaxCadence();
+ method public double getMinCadence();
+ property public final double maxCadence;
+ property public final double minCadence;
+ }
+
+ public static final class ExercisePerformanceTarget.HeartRateTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.HeartRateTarget(double minHeartRate, double maxHeartRate);
+ method public double getMaxHeartRate();
+ method public double getMinHeartRate();
+ property public final double maxHeartRate;
+ property public final double minHeartRate;
+ }
+
+ public static final class ExercisePerformanceTarget.PowerTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.PowerTarget(androidx.health.connect.client.units.Power minPower, androidx.health.connect.client.units.Power maxPower);
+ method public androidx.health.connect.client.units.Power getMaxPower();
+ method public androidx.health.connect.client.units.Power getMinPower();
+ property public final androidx.health.connect.client.units.Power maxPower;
+ property public final androidx.health.connect.client.units.Power minPower;
+ }
+
+ public static final class ExercisePerformanceTarget.RateOfPerceivedExertionTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.RateOfPerceivedExertionTarget(int rpe);
+ method public int getRpe();
+ property public final int rpe;
+ }
+
+ public static final class ExercisePerformanceTarget.SpeedTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.SpeedTarget(androidx.health.connect.client.units.Velocity minSpeed, androidx.health.connect.client.units.Velocity maxSpeed);
+ method public androidx.health.connect.client.units.Velocity getMaxSpeed();
+ method public androidx.health.connect.client.units.Velocity getMinSpeed();
+ property public final androidx.health.connect.client.units.Velocity maxSpeed;
+ property public final androidx.health.connect.client.units.Velocity minSpeed;
+ }
+
+ public static final class ExercisePerformanceTarget.UnknownTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ field public static final androidx.health.connect.client.records.ExercisePerformanceTarget.UnknownTarget INSTANCE;
+ }
+
+ public static final class ExercisePerformanceTarget.WeightTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.WeightTarget(androidx.health.connect.client.units.Mass mass);
+ method public androidx.health.connect.client.units.Mass getMass();
+ property public final androidx.health.connect.client.units.Mass mass;
+ }
+
public final class ExerciseRoute {
ctor public ExerciseRoute(java.util.List<androidx.health.connect.client.records.ExerciseRoute.Location> route);
method public java.util.List<androidx.health.connect.client.records.ExerciseRoute.Location> getRoute();
@@ -612,6 +722,7 @@
ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments);
ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments, optional java.util.List<androidx.health.connect.client.records.ExerciseLap> laps);
ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments, optional java.util.List<androidx.health.connect.client.records.ExerciseLap> laps, optional androidx.health.connect.client.records.ExerciseRoute? exerciseRoute);
+ ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments, optional java.util.List<androidx.health.connect.client.records.ExerciseLap> laps, optional androidx.health.connect.client.records.ExerciseRoute? exerciseRoute, optional String? plannedExerciseSessionId);
method public java.time.Instant getEndTime();
method public java.time.ZoneOffset? getEndZoneOffset();
method public androidx.health.connect.client.records.ExerciseRouteResult getExerciseRouteResult();
@@ -619,6 +730,7 @@
method public java.util.List<androidx.health.connect.client.records.ExerciseLap> getLaps();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public String? getNotes();
+ method public String? getPlannedExerciseSessionId();
method public java.util.List<androidx.health.connect.client.records.ExerciseSegment> getSegments();
method public java.time.Instant getStartTime();
method public java.time.ZoneOffset? getStartZoneOffset();
@@ -630,6 +742,7 @@
property public final java.util.List<androidx.health.connect.client.records.ExerciseLap> laps;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final String? notes;
+ property public final String? plannedExerciseSessionId;
property public final java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments;
property public java.time.Instant startTime;
property public java.time.ZoneOffset? startZoneOffset;
@@ -1052,6 +1165,77 @@
property public java.time.ZoneOffset? zoneOffset;
}
+ public final class PlannedExerciseBlock {
+ ctor public PlannedExerciseBlock(int repetitions, java.util.List<androidx.health.connect.client.records.PlannedExerciseStep> steps, optional String? description);
+ method public String? getDescription();
+ method public int getRepetitions();
+ method public java.util.List<androidx.health.connect.client.records.PlannedExerciseStep> getSteps();
+ property public final String? description;
+ property public final int repetitions;
+ property public final java.util.List<androidx.health.connect.client.records.PlannedExerciseStep> steps;
+ }
+
+ public final class PlannedExerciseSessionRecord implements androidx.health.connect.client.records.Record {
+ ctor public PlannedExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType);
+ ctor public PlannedExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title);
+ ctor public PlannedExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title, optional String? notes);
+ ctor public PlannedExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public PlannedExerciseSessionRecord(java.time.LocalDate startDate, java.time.Duration duration, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType);
+ ctor public PlannedExerciseSessionRecord(java.time.LocalDate startDate, java.time.Duration duration, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title);
+ ctor public PlannedExerciseSessionRecord(java.time.LocalDate startDate, java.time.Duration duration, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title, optional String? notes);
+ ctor public PlannedExerciseSessionRecord(java.time.LocalDate startDate, java.time.Duration duration, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> getBlocks();
+ method public String? getCompletedExerciseSessionId();
+ method public java.time.Instant getEndTime();
+ method public java.time.ZoneOffset? getEndZoneOffset();
+ method public int getExerciseType();
+ method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+ method public String? getNotes();
+ method public java.time.Instant getStartTime();
+ method public java.time.ZoneOffset? getStartZoneOffset();
+ method public String? getTitle();
+ method public boolean hasExplicitTime();
+ property public final java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks;
+ property public final String? completedExerciseSessionId;
+ property public java.time.Instant endTime;
+ property public java.time.ZoneOffset? endZoneOffset;
+ property public final int exerciseType;
+ property public final boolean hasExplicitTime;
+ property public androidx.health.connect.client.records.metadata.Metadata metadata;
+ property public final String? notes;
+ property public java.time.Instant startTime;
+ property public java.time.ZoneOffset? startZoneOffset;
+ property public final String? title;
+ field public static final androidx.health.connect.client.records.PlannedExerciseSessionRecord.Companion Companion;
+ }
+
+ public static final class PlannedExerciseSessionRecord.Companion {
+ }
+
+ public final class PlannedExerciseStep {
+ ctor public PlannedExerciseStep(int exerciseType, int exercisePhase, androidx.health.connect.client.records.ExerciseCompletionGoal completionGoal, java.util.List<? extends androidx.health.connect.client.records.ExercisePerformanceTarget> performanceTargets, optional String? description);
+ method public androidx.health.connect.client.records.ExerciseCompletionGoal getCompletionGoal();
+ method public String? getDescription();
+ method public int getExercisePhase();
+ method public int getExerciseType();
+ method public java.util.List<androidx.health.connect.client.records.ExercisePerformanceTarget> getPerformanceTargets();
+ property public final androidx.health.connect.client.records.ExerciseCompletionGoal completionGoal;
+ property public final String? description;
+ property public final int exercisePhase;
+ property public final int exerciseType;
+ property public final java.util.List<androidx.health.connect.client.records.ExercisePerformanceTarget> performanceTargets;
+ field public static final androidx.health.connect.client.records.PlannedExerciseStep.Companion Companion;
+ field public static final int EXERCISE_PHASE_ACTIVE = 3; // 0x3
+ field public static final int EXERCISE_PHASE_COOLDOWN = 4; // 0x4
+ field public static final int EXERCISE_PHASE_RECOVERY = 5; // 0x5
+ field public static final int EXERCISE_PHASE_REST = 2; // 0x2
+ field public static final int EXERCISE_PHASE_UNKNOWN = 0; // 0x0
+ field public static final int EXERCISE_PHASE_WARMUP = 1; // 0x1
+ }
+
+ public static final class PlannedExerciseStep.Companion {
+ }
+
public final class PowerRecord implements androidx.health.connect.client.records.Record {
ctor public PowerRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PowerRecord.Sample> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public java.time.Instant getEndTime();
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 501a83c..4eefc3f 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -467,6 +467,61 @@
public static final class ElevationGainedRecord.Companion {
}
+ public abstract class ExerciseCompletionGoal {
+ }
+
+ public static final class ExerciseCompletionGoal.ActiveCaloriesBurnedGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.ActiveCaloriesBurnedGoal(androidx.health.connect.client.units.Energy activeCalories);
+ method public androidx.health.connect.client.units.Energy getActiveCalories();
+ property public final androidx.health.connect.client.units.Energy activeCalories;
+ }
+
+ public static final class ExerciseCompletionGoal.DistanceAndDurationGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.DistanceAndDurationGoal(androidx.health.connect.client.units.Length distance, java.time.Duration duration);
+ method public androidx.health.connect.client.units.Length getDistance();
+ method public java.time.Duration getDuration();
+ property public final androidx.health.connect.client.units.Length distance;
+ property public final java.time.Duration duration;
+ }
+
+ public static final class ExerciseCompletionGoal.DistanceGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.DistanceGoal(androidx.health.connect.client.units.Length distance);
+ method public androidx.health.connect.client.units.Length getDistance();
+ property public final androidx.health.connect.client.units.Length distance;
+ }
+
+ public static final class ExerciseCompletionGoal.DurationGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.DurationGoal(java.time.Duration duration);
+ method public java.time.Duration getDuration();
+ property public final java.time.Duration duration;
+ }
+
+ public static final class ExerciseCompletionGoal.ManualCompletion extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ field public static final androidx.health.connect.client.records.ExerciseCompletionGoal.ManualCompletion INSTANCE;
+ }
+
+ public static final class ExerciseCompletionGoal.RepetitionsGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.RepetitionsGoal(java.time.Duration repetitions);
+ method public java.time.Duration getRepetitions();
+ property public final java.time.Duration repetitions;
+ }
+
+ public static final class ExerciseCompletionGoal.StepsGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.StepsGoal(int steps);
+ method public int getSteps();
+ property public final int steps;
+ }
+
+ public static final class ExerciseCompletionGoal.TotalCaloriesBurnedGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ ctor public ExerciseCompletionGoal.TotalCaloriesBurnedGoal(androidx.health.connect.client.units.Energy totalCalories);
+ method public androidx.health.connect.client.units.Energy getTotalCalories();
+ property public final androidx.health.connect.client.units.Energy totalCalories;
+ }
+
+ public static final class ExerciseCompletionGoal.UnknownGoal extends androidx.health.connect.client.records.ExerciseCompletionGoal {
+ field public static final androidx.health.connect.client.records.ExerciseCompletionGoal.UnknownGoal INSTANCE;
+ }
+
public final class ExerciseLap {
ctor public ExerciseLap(java.time.Instant startTime, java.time.Instant endTime, optional androidx.health.connect.client.units.Length? length);
method public java.time.Instant getEndTime();
@@ -477,6 +532,61 @@
property public final java.time.Instant startTime;
}
+ public abstract class ExercisePerformanceTarget {
+ }
+
+ public static final class ExercisePerformanceTarget.AmrapTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ field public static final androidx.health.connect.client.records.ExercisePerformanceTarget.AmrapTarget INSTANCE;
+ }
+
+ public static final class ExercisePerformanceTarget.CadenceTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.CadenceTarget(double minCadence, double maxCadence);
+ method public double getMaxCadence();
+ method public double getMinCadence();
+ property public final double maxCadence;
+ property public final double minCadence;
+ }
+
+ public static final class ExercisePerformanceTarget.HeartRateTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.HeartRateTarget(double minHeartRate, double maxHeartRate);
+ method public double getMaxHeartRate();
+ method public double getMinHeartRate();
+ property public final double maxHeartRate;
+ property public final double minHeartRate;
+ }
+
+ public static final class ExercisePerformanceTarget.PowerTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.PowerTarget(androidx.health.connect.client.units.Power minPower, androidx.health.connect.client.units.Power maxPower);
+ method public androidx.health.connect.client.units.Power getMaxPower();
+ method public androidx.health.connect.client.units.Power getMinPower();
+ property public final androidx.health.connect.client.units.Power maxPower;
+ property public final androidx.health.connect.client.units.Power minPower;
+ }
+
+ public static final class ExercisePerformanceTarget.RateOfPerceivedExertionTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.RateOfPerceivedExertionTarget(int rpe);
+ method public int getRpe();
+ property public final int rpe;
+ }
+
+ public static final class ExercisePerformanceTarget.SpeedTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.SpeedTarget(androidx.health.connect.client.units.Velocity minSpeed, androidx.health.connect.client.units.Velocity maxSpeed);
+ method public androidx.health.connect.client.units.Velocity getMaxSpeed();
+ method public androidx.health.connect.client.units.Velocity getMinSpeed();
+ property public final androidx.health.connect.client.units.Velocity maxSpeed;
+ property public final androidx.health.connect.client.units.Velocity minSpeed;
+ }
+
+ public static final class ExercisePerformanceTarget.UnknownTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ field public static final androidx.health.connect.client.records.ExercisePerformanceTarget.UnknownTarget INSTANCE;
+ }
+
+ public static final class ExercisePerformanceTarget.WeightTarget extends androidx.health.connect.client.records.ExercisePerformanceTarget {
+ ctor public ExercisePerformanceTarget.WeightTarget(androidx.health.connect.client.units.Mass mass);
+ method public androidx.health.connect.client.units.Mass getMass();
+ property public final androidx.health.connect.client.units.Mass mass;
+ }
+
public final class ExerciseRoute {
ctor public ExerciseRoute(java.util.List<androidx.health.connect.client.records.ExerciseRoute.Location> route);
method public java.util.List<androidx.health.connect.client.records.ExerciseRoute.Location> getRoute();
@@ -612,6 +722,7 @@
ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments);
ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments, optional java.util.List<androidx.health.connect.client.records.ExerciseLap> laps);
ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments, optional java.util.List<androidx.health.connect.client.records.ExerciseLap> laps, optional androidx.health.connect.client.records.ExerciseRoute? exerciseRoute);
+ ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata, optional java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments, optional java.util.List<androidx.health.connect.client.records.ExerciseLap> laps, optional androidx.health.connect.client.records.ExerciseRoute? exerciseRoute, optional String? plannedExerciseSessionId);
method public java.time.Instant getEndTime();
method public java.time.ZoneOffset? getEndZoneOffset();
method public androidx.health.connect.client.records.ExerciseRouteResult getExerciseRouteResult();
@@ -619,6 +730,7 @@
method public java.util.List<androidx.health.connect.client.records.ExerciseLap> getLaps();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public String? getNotes();
+ method public String? getPlannedExerciseSessionId();
method public java.util.List<androidx.health.connect.client.records.ExerciseSegment> getSegments();
method public java.time.Instant getStartTime();
method public java.time.ZoneOffset? getStartZoneOffset();
@@ -630,6 +742,7 @@
property public final java.util.List<androidx.health.connect.client.records.ExerciseLap> laps;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final String? notes;
+ property public final String? plannedExerciseSessionId;
property public final java.util.List<androidx.health.connect.client.records.ExerciseSegment> segments;
property public java.time.Instant startTime;
property public java.time.ZoneOffset? startZoneOffset;
@@ -1070,6 +1183,77 @@
property public java.time.ZoneOffset? zoneOffset;
}
+ public final class PlannedExerciseBlock {
+ ctor public PlannedExerciseBlock(int repetitions, java.util.List<androidx.health.connect.client.records.PlannedExerciseStep> steps, optional String? description);
+ method public String? getDescription();
+ method public int getRepetitions();
+ method public java.util.List<androidx.health.connect.client.records.PlannedExerciseStep> getSteps();
+ property public final String? description;
+ property public final int repetitions;
+ property public final java.util.List<androidx.health.connect.client.records.PlannedExerciseStep> steps;
+ }
+
+ public final class PlannedExerciseSessionRecord implements androidx.health.connect.client.records.IntervalRecord {
+ ctor public PlannedExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType);
+ ctor public PlannedExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title);
+ ctor public PlannedExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title, optional String? notes);
+ ctor public PlannedExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public PlannedExerciseSessionRecord(java.time.LocalDate startDate, java.time.Duration duration, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType);
+ ctor public PlannedExerciseSessionRecord(java.time.LocalDate startDate, java.time.Duration duration, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title);
+ ctor public PlannedExerciseSessionRecord(java.time.LocalDate startDate, java.time.Duration duration, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title, optional String? notes);
+ ctor public PlannedExerciseSessionRecord(java.time.LocalDate startDate, java.time.Duration duration, java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> getBlocks();
+ method public String? getCompletedExerciseSessionId();
+ method public java.time.Instant getEndTime();
+ method public java.time.ZoneOffset? getEndZoneOffset();
+ method public int getExerciseType();
+ method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+ method public String? getNotes();
+ method public java.time.Instant getStartTime();
+ method public java.time.ZoneOffset? getStartZoneOffset();
+ method public String? getTitle();
+ method public boolean hasExplicitTime();
+ property public final java.util.List<androidx.health.connect.client.records.PlannedExerciseBlock> blocks;
+ property public final String? completedExerciseSessionId;
+ property public java.time.Instant endTime;
+ property public java.time.ZoneOffset? endZoneOffset;
+ property public final int exerciseType;
+ property public final boolean hasExplicitTime;
+ property public androidx.health.connect.client.records.metadata.Metadata metadata;
+ property public final String? notes;
+ property public java.time.Instant startTime;
+ property public java.time.ZoneOffset? startZoneOffset;
+ property public final String? title;
+ field public static final androidx.health.connect.client.records.PlannedExerciseSessionRecord.Companion Companion;
+ }
+
+ public static final class PlannedExerciseSessionRecord.Companion {
+ }
+
+ public final class PlannedExerciseStep {
+ ctor public PlannedExerciseStep(int exerciseType, int exercisePhase, androidx.health.connect.client.records.ExerciseCompletionGoal completionGoal, java.util.List<? extends androidx.health.connect.client.records.ExercisePerformanceTarget> performanceTargets, optional String? description);
+ method public androidx.health.connect.client.records.ExerciseCompletionGoal getCompletionGoal();
+ method public String? getDescription();
+ method public int getExercisePhase();
+ method public int getExerciseType();
+ method public java.util.List<androidx.health.connect.client.records.ExercisePerformanceTarget> getPerformanceTargets();
+ property public final androidx.health.connect.client.records.ExerciseCompletionGoal completionGoal;
+ property public final String? description;
+ property public final int exercisePhase;
+ property public final int exerciseType;
+ property public final java.util.List<androidx.health.connect.client.records.ExercisePerformanceTarget> performanceTargets;
+ field public static final androidx.health.connect.client.records.PlannedExerciseStep.Companion Companion;
+ field public static final int EXERCISE_PHASE_ACTIVE = 3; // 0x3
+ field public static final int EXERCISE_PHASE_COOLDOWN = 4; // 0x4
+ field public static final int EXERCISE_PHASE_RECOVERY = 5; // 0x5
+ field public static final int EXERCISE_PHASE_REST = 2; // 0x2
+ field public static final int EXERCISE_PHASE_UNKNOWN = 0; // 0x0
+ field public static final int EXERCISE_PHASE_WARMUP = 1; // 0x1
+ }
+
+ public static final class PlannedExerciseStep.Companion {
+ }
+
public final class PowerRecord implements androidx.health.connect.client.records.SeriesRecord<androidx.health.connect.client.records.PowerRecord.Sample> {
ctor public PowerRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.PowerRecord.Sample> samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public java.time.Instant getEndTime();
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 d277f62f..1394ec9 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
@@ -43,6 +43,7 @@
import androidx.health.connect.client.records.NutritionRecord
import androidx.health.connect.client.records.OvulationTestRecord
import androidx.health.connect.client.records.OxygenSaturationRecord
+import androidx.health.connect.client.records.PlannedExerciseSessionRecord
import androidx.health.connect.client.records.PowerRecord
import androidx.health.connect.client.records.Record
import androidx.health.connect.client.records.RespiratoryRateRecord
@@ -174,6 +175,7 @@
PERMISSION_PREFIX + "READ_TOTAL_CALORIES_BURNED"
internal const val READ_VO2_MAX = PERMISSION_PREFIX + "READ_VO2_MAX"
internal const val READ_WHEELCHAIR_PUSHES = PERMISSION_PREFIX + "READ_WHEELCHAIR_PUSHES"
+ internal const val READ_PLANNED_EXERCISE = PERMISSION_PREFIX + "READ_PLANNED_EXERCISE"
internal const val READ_POWER = PERMISSION_PREFIX + "READ_POWER"
internal const val READ_SPEED = PERMISSION_PREFIX + "READ_SPEED"
@@ -232,6 +234,7 @@
PERMISSION_PREFIX + "WRITE_TOTAL_CALORIES_BURNED"
internal const val WRITE_VO2_MAX = PERMISSION_PREFIX + "WRITE_VO2_MAX"
internal const val WRITE_WHEELCHAIR_PUSHES = PERMISSION_PREFIX + "WRITE_WHEELCHAIR_PUSHES"
+ internal const val WRITE_PLANNED_EXERCISE = PERMISSION_PREFIX + "WRITE_PLANNED_EXERCISE"
internal const val WRITE_POWER = PERMISSION_PREFIX + "WRITE_POWER"
internal const val WRITE_SPEED = PERMISSION_PREFIX + "WRITE_SPEED"
@@ -328,6 +331,8 @@
READ_OVULATION_TEST.substringAfter(READ_PERMISSION_PREFIX),
OxygenSaturationRecord::class to
READ_OXYGEN_SATURATION.substringAfter(READ_PERMISSION_PREFIX),
+ PlannedExerciseSessionRecord::class to
+ READ_PLANNED_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
PowerRecord::class to READ_POWER.substringAfter(READ_PERMISSION_PREFIX),
RespiratoryRateRecord::class to
READ_RESPIRATORY_RATE.substringAfter(READ_PERMISSION_PREFIX),
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseCompletionGoal.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseCompletionGoal.kt
new file mode 100644
index 0000000..f369b20
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseCompletionGoal.kt
@@ -0,0 +1,198 @@
+/*
+ * 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.records
+
+import androidx.health.connect.client.units.Energy
+import androidx.health.connect.client.units.Length
+import java.time.Duration
+
+/** A goal which should be met to complete a [PlannedExerciseStep]. */
+abstract class ExerciseCompletionGoal internal constructor() {
+ /** An [ExerciseCompletionGoal] that requires covering a specified distance. */
+ class DistanceGoal(
+ val distance: Length,
+ ) : ExerciseCompletionGoal() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is DistanceGoal) return false
+
+ return distance == other.distance
+ }
+
+ override fun hashCode(): Int {
+ return distance.hashCode()
+ }
+
+ override fun toString(): String {
+ return "DistanceGoal(distance=$distance)"
+ }
+ }
+
+ /**
+ * An [ExerciseCompletionGoal] that requires covering a specified distance. Additionally, the
+ * step is not complete until the specified time has elapsed. Time remaining after the specified
+ * distance has been completed should be spent resting. In the context of swimming, this is
+ * sometimes referred to as 'interval training'.
+ *
+ * <p>For example, a swimming coach may specify '100m @ 1min40s'. This implies: complete 100m
+ * and if you manage it in 1min30s, you will have 10s of rest prior to the next set.
+ */
+ class DistanceAndDurationGoal(
+ val distance: Length,
+ val duration: Duration,
+ ) : ExerciseCompletionGoal() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is DistanceAndDurationGoal) return false
+
+ return distance == other.distance && duration == other.duration
+ }
+
+ override fun toString(): String {
+ return "DistanceAndDurationGoal(distance=$distance, duration=$duration)"
+ }
+
+ override fun hashCode(): Int {
+ var result = distance.hashCode()
+ result = 31 * result + duration.hashCode()
+ return result
+ }
+ }
+
+ /** An [ExerciseCompletionGoal] that requires completing a specified number of steps. */
+ class StepsGoal(
+ val steps: Int,
+ ) : ExerciseCompletionGoal() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is StepsGoal) return false
+
+ return steps == other.steps
+ }
+
+ override fun hashCode(): Int {
+ return steps
+ }
+
+ override fun toString(): String {
+ return "StepsGoal(steps=$steps)"
+ }
+ }
+
+ /** An [ExerciseCompletionGoal] that requires a specified duration to elapse. */
+ class DurationGoal(
+ val duration: Duration,
+ ) : ExerciseCompletionGoal() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is DurationGoal) return false
+
+ return duration == other.duration
+ }
+
+ override fun hashCode(): Int {
+ return duration.hashCode()
+ }
+
+ override fun toString(): String {
+ return "DurationGoal(duration=$duration)"
+ }
+ }
+
+ /**
+ * An [ExerciseCompletionGoal] that requires a specified number of repetitions to be completed.
+ */
+ class RepetitionsGoal(
+ val repetitions: Duration,
+ ) : ExerciseCompletionGoal() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is RepetitionsGoal) return false
+
+ return repetitions == other.repetitions
+ }
+
+ override fun hashCode(): Int {
+ return repetitions.hashCode()
+ }
+
+ override fun toString(): String {
+ return "RepetitionsGoal(repetitions=$repetitions)"
+ }
+ }
+
+ /**
+ * An [ExerciseCompletionGoal] that requires a specified number of total calories to be burned.
+ */
+ class TotalCaloriesBurnedGoal(
+ val totalCalories: Energy,
+ ) : ExerciseCompletionGoal() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is TotalCaloriesBurnedGoal) return false
+
+ return totalCalories == other.totalCalories
+ }
+
+ override fun hashCode(): Int {
+ return totalCalories.hashCode()
+ }
+
+ override fun toString(): String {
+ return "TotalCaloriesBurnedGoal(totalCalories=$totalCalories)"
+ }
+ }
+
+ /**
+ * An [ExerciseCompletionGoal] that requires a specified number of active calories to be burned.
+ */
+ class ActiveCaloriesBurnedGoal(
+ val activeCalories: Energy,
+ ) : ExerciseCompletionGoal() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ActiveCaloriesBurnedGoal) return false
+
+ return activeCalories == other.activeCalories
+ }
+
+ override fun hashCode(): Int {
+ return activeCalories.hashCode()
+ }
+
+ override fun toString(): String {
+ return "ActiveCaloriesBurnedGoal(activeCalories=$activeCalories)"
+ }
+ }
+
+ /** An [ExerciseCompletionGoal] that is unknown. */
+ object UnknownGoal : ExerciseCompletionGoal() {
+ override fun toString(): String {
+ return "UnknownGoal()"
+ }
+ }
+
+ /**
+ * An [ExerciseCompletionGoal] that has no specific target metric. It is up to the user to
+ * determine when the associated [PlannedExerciseStep] is complete, typically based upon some
+ * instruction in the [PlannedExerciseStep.description] field.
+ */
+ object ManualCompletion : ExerciseCompletionGoal() {
+ override fun toString(): String {
+ return "ManualCompletion()"
+ }
+ }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExercisePerformanceTarget.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExercisePerformanceTarget.kt
new file mode 100644
index 0000000..35cb1ff
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExercisePerformanceTarget.kt
@@ -0,0 +1,212 @@
+/*
+ * 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.records
+
+import androidx.health.connect.client.units.Mass
+import androidx.health.connect.client.units.Power
+import androidx.health.connect.client.units.Velocity
+
+/** An ongoing target that should be met during a [PlannedExerciseStep]. */
+abstract class ExercisePerformanceTarget internal constructor() {
+ /**
+ * An [ExercisePerformanceTarget] that requires a target power range to be met during the
+ * associated [PlannedExerciseStep].
+ */
+ class PowerTarget(
+ val minPower: Power,
+ val maxPower: Power,
+ ) : ExercisePerformanceTarget() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is PowerTarget) return false
+
+ if (minPower != other.minPower) return false
+ if (maxPower != other.maxPower) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = minPower.hashCode()
+ result = 31 * result + maxPower.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "PowerTarget(minPower=$minPower, maxPower=$maxPower)"
+ }
+ }
+
+ /**
+ * An [ExercisePerformanceTarget] that requires a target speed range to be met during the
+ * associated [PlannedExerciseStep].
+ */
+ class SpeedTarget(
+ val minSpeed: Velocity,
+ val maxSpeed: Velocity,
+ ) : ExercisePerformanceTarget() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SpeedTarget) return false
+
+ if (minSpeed != other.minSpeed) return false
+ if (maxSpeed != other.maxSpeed) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = minSpeed.hashCode()
+ result = 31 * result + maxSpeed.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "SpeedTarget(minSpeed=$minSpeed, maxSpeed=$maxSpeed)"
+ }
+ }
+
+ /**
+ * An [ExercisePerformanceTarget] that requires a target cadence range to be met during the
+ * associated [PlannedExerciseStep].The value may be interpreted as RPM for e.g. cycling
+ * activities, or as steps per minute for e.g. walking/running activities.
+ */
+ class CadenceTarget(
+ val minCadence: Double,
+ val maxCadence: Double,
+ ) : ExercisePerformanceTarget() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is CadenceTarget) return false
+
+ if (minCadence != other.minCadence) return false
+ if (maxCadence != other.maxCadence) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = minCadence.hashCode()
+ result = 31 * result + maxCadence.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "CadenceTarget(minCadence=$minCadence, maxCadence=$maxCadence)"
+ }
+ }
+
+ /**
+ * An [ExercisePerformanceTarget] that requires a target heart rate range, in BPM, to be met
+ * during the associated {@link PlannedExerciseStep}.
+ */
+ class HeartRateTarget(
+ val minHeartRate: Double,
+ val maxHeartRate: Double,
+ ) : ExercisePerformanceTarget() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is HeartRateTarget) return false
+
+ if (minHeartRate != other.minHeartRate) return false
+ if (maxHeartRate != other.maxHeartRate) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = minHeartRate.hashCode()
+ result = 31 * result + maxHeartRate.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "HeartRateTarget(minHeartRate=$minHeartRate, maxHeartRate=$maxHeartRate)"
+ }
+ }
+
+ /**
+ * An [ExercisePerformanceTarget] that requires a target weight to be lifted during the
+ * associated [PlannedExerciseStep].
+ */
+ class WeightTarget(
+ val mass: Mass,
+ ) : ExercisePerformanceTarget() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is WeightTarget) return false
+
+ return mass == other.mass
+ }
+
+ override fun hashCode(): Int {
+ return mass.hashCode()
+ }
+
+ override fun toString(): String {
+ return "WeightTarget(mass=$mass)"
+ }
+ }
+
+ /**
+ * An [ExercisePerformanceTarget] that requires a target RPE (rate of perceived exertion) to be
+ * met during the associated {@link PlannedExerciseStep}.
+ *
+ * <p>Values correspond to the Borg CR10 RPE scale and must be in the range 0 to 10 inclusive.
+ * 0: No exertion (at rest) 1: Very light 2-3: Light 4-5: Moderate 6-7: Hard 8-9: Very hard 10:
+ * Maximum effort
+ */
+ class RateOfPerceivedExertionTarget(
+ val rpe: Int,
+ ) : ExercisePerformanceTarget() {
+ init {
+ require(rpe in 0..10) { "RPE value must be between 0 and 10, inclusive." }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is RateOfPerceivedExertionTarget) return false
+
+ return rpe == other.rpe
+ }
+
+ override fun hashCode(): Int {
+ return rpe.hashCode()
+ }
+
+ override fun toString(): String {
+ return "RateOfPerceivedExertionTarget(rpe=$rpe)"
+ }
+ }
+
+ /**
+ * An [ExercisePerformanceTarget] that requires completing as many repetitions as possible.
+ * AMRAP (as many reps as possible) sets are often used in conjunction with a duration based
+ * completion goal.
+ */
+ object AmrapTarget : ExercisePerformanceTarget() {
+ override fun toString(): String {
+ return "AmrapTarget()"
+ }
+ }
+
+ /** An [ExercisePerformanceTarget] that is unknown. */
+ object UnknownTarget : ExercisePerformanceTarget() {
+ override fun toString(): String {
+ return "UnknownTarget()"
+ }
+ }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
index 8d7a3cf..9792a39 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
@@ -65,6 +65,7 @@
* session.
*/
val exerciseRouteResult: ExerciseRouteResult = ExerciseRouteResult.NoData(),
+ val plannedExerciseSessionId: String? = null
) : IntervalRecord {
@JvmOverloads
@@ -83,6 +84,8 @@
segments: List<ExerciseSegment> = emptyList(),
laps: List<ExerciseLap> = emptyList(),
exerciseRoute: ExerciseRoute? = null,
+ /** The planned exercise session this workout was based upon. Optional field. */
+ plannedExerciseSessionId: String? = null,
) : this(
startTime,
startZoneOffset,
@@ -94,7 +97,8 @@
metadata,
segments,
laps,
- exerciseRoute?.let { ExerciseRouteResult.Data(it) } ?: ExerciseRouteResult.NoData()
+ exerciseRoute?.let { ExerciseRouteResult.Data(it) } ?: ExerciseRouteResult.NoData(),
+ plannedExerciseSessionId,
)
init {
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/PlannedExerciseBlock.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/PlannedExerciseBlock.kt
new file mode 100644
index 0000000..555506c
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/PlannedExerciseBlock.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.records
+/** Represents a series of [PlannedExerciseStep]s. Part of a [PlannedExerciseSessionRecord]. */
+class PlannedExerciseBlock(
+ val repetitions: Int,
+ val steps: List<PlannedExerciseStep>,
+ val description: String? = null,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is PlannedExerciseBlock) return false
+
+ if (repetitions != other.repetitions) return false
+ if (description != other.description) return false
+ if (steps != other.steps) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = repetitions
+ result = 31 * result + (description?.hashCode() ?: 0)
+ result = 31 * result + steps.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "PlannedExerciseBlock(repetitions=$repetitions, description=$description, steps=$steps)"
+ }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/PlannedExerciseSessionRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/PlannedExerciseSessionRecord.kt
new file mode 100644
index 0000000..b6a8a2f
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/PlannedExerciseSessionRecord.kt
@@ -0,0 +1,189 @@
+/*
+ * 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.records
+
+import androidx.health.connect.client.records.metadata.Metadata
+import java.time.Duration
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.ZoneId
+import java.time.ZoneOffset
+
+/**
+ * Captures a planned exercise session, also commonly referred to as a training plan.
+ *
+ * <p>Each record contains a start time, end time, an exercise type and a list of
+ * [PlannedExerciseBlock] which describe the details of the planned session. The start and end times
+ * may be in the future.
+ */
+class PlannedExerciseSessionRecord
+internal constructor(
+ override val startTime: Instant,
+ override val startZoneOffset: ZoneOffset?,
+ override val endTime: Instant,
+ override val endZoneOffset: ZoneOffset?,
+ override val metadata: Metadata,
+ @get:JvmName("hasExplicitTime") val hasExplicitTime: Boolean,
+ /** Type of exercise (e.g. walking, swimming). Required field. */
+ @property:ExerciseSessionRecord.ExerciseTypes val exerciseType: Int,
+ /** The exercise session that completed this planned session. */
+ val completedExerciseSessionId: String?,
+ val blocks: List<PlannedExerciseBlock>,
+ /** Title of the session. Optional field. */
+ val title: String? = null,
+ /** Additional notes for the session. Optional field. */
+ val notes: String? = null,
+) : IntervalRecord {
+ /**
+ * Constructor that accepts a physical time and zone offset.
+ *
+ * @param startTime The time at which the session should start.
+ * @param startZoneOffset The zone offset at the start of this session. If null is provided,
+ * this will default to the current system timezone.
+ * @param endTime The time at which the session should end.
+ * @param endZoneOffset The zone offset at the end of this session. If null is provided, this
+ * will default to the current system timezone.
+ * @param blocks The [PlannedExerciseBlock]s that contain details of this session.
+ * @param title The title of this session.
+ * @param notes Notes for this session.
+ * @param exerciseType The exercise type of this session.
+ * @param metadata Metadata for this session.
+ */
+ @JvmOverloads
+ constructor(
+ startTime: Instant,
+ startZoneOffset: ZoneOffset?,
+ endTime: Instant,
+ endZoneOffset: ZoneOffset?,
+ blocks: List<PlannedExerciseBlock>,
+ /** Type of exercise (e.g. walking, swimming). Required field. */
+ exerciseType: Int,
+ /** Title of the session. Optional field. */
+ title: String? = null,
+ /** Additional notes for the session. Optional field. */
+ notes: String? = null,
+ metadata: Metadata = Metadata.EMPTY,
+ ) : this(
+ startTime,
+ startZoneOffset,
+ endTime,
+ endZoneOffset,
+ metadata,
+ true,
+ exerciseType,
+ null,
+ blocks,
+ title = title,
+ notes = notes,
+ )
+
+ /**
+ * Constructor that accepts a local date plus a duration. The start time will be implicitly
+ * generated from a local time at noon in the system default timezone on the day specified by
+ * [startDate]. The end time will be generated by adding [duration] to the start time.
+ *
+ * @param startDate The date on which the session should occur.
+ * @param duration The expected or estimated duration of the session.
+ * @param blocks The [PlannedExerciseBlock]s that contain details of this session.
+ * @param title The title of this session.
+ * @param notes Notes for this session.
+ * @param exerciseType The exercise type of this session.
+ * @param metadata Metadata for this session.
+ */
+ @JvmOverloads
+ constructor(
+ startDate: LocalDate,
+ duration: Duration,
+ blocks: List<PlannedExerciseBlock>,
+ /** Type of exercise (e.g. walking, swimming). Required field. */
+ exerciseType: Int,
+ /** Title of the session. Optional field. */
+ title: String? = null,
+ /** Additional notes for the session. Optional field. */
+ notes: String? = null,
+ metadata: Metadata = Metadata.EMPTY,
+ ) : this(
+ startDate.toPhysicalTimeAtNoon(),
+ startDate.toPhysicalTimeAtNoon().getOffset(),
+ startDate.toPhysicalTimeAtNoon().plus(duration),
+ startDate.toPhysicalTimeAtNoon().plus(duration).getOffset(),
+ metadata,
+ false,
+ exerciseType,
+ null,
+ blocks,
+ title = title,
+ notes = notes,
+ )
+
+ init {
+ require(startTime.isBefore(endTime))
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is PlannedExerciseSessionRecord) return false
+
+ if (startTime != other.startTime) return false
+ if (startZoneOffset != other.startZoneOffset) return false
+ if (endTime != other.endTime) return false
+ if (endZoneOffset != other.endZoneOffset) return false
+ if (hasExplicitTime != other.hasExplicitTime) return false
+ if (blocks != other.blocks) return false
+ if (title != other.title) return false
+ if (notes != other.notes) return false
+ if (exerciseType != other.exerciseType) return false
+ if (metadata != other.metadata) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = startTime.hashCode()
+ result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
+ result = 31 * result + endTime.hashCode()
+ result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
+ result = 31 * result + hasExplicitTime.hashCode()
+ result = 31 * result + blocks.hashCode()
+ result = 31 * result + (title?.hashCode() ?: 0)
+ result = 31 * result + (notes?.hashCode() ?: 0)
+ result = 31 * result + exerciseType
+ result = 31 * result + (completedExerciseSessionId?.hashCode() ?: 0)
+ result = 31 * result + metadata.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "PlannedExerciseSessionRecord(startTime=$startTime, startZoneOffset=$startZoneOffset, endTime=$endTime, endZoneOffset=$endZoneOffset, hasExplicitTime=$hasExplicitTime, title=$title, notes=$notes, exerciseType=$exerciseType, completedExerciseSessionId=$completedExerciseSessionId, metadata=$metadata, blocks=$blocks)"
+ }
+
+ companion object {
+ /**
+ * Converts a local date to a physical timestamp by assuming a fixed time at noon and the
+ * current system time zone.
+ */
+ private fun LocalDate.toPhysicalTimeAtNoon(): Instant {
+ return this.atTime(LocalTime.NOON).atZone(ZoneId.systemDefault()).toInstant()
+ }
+
+ /** Gets the offset of the system default timezone at the given [Instant]. */
+ private fun Instant.getOffset(): ZoneOffset {
+ return ZoneOffset.systemDefault().rules.getOffset(this)
+ }
+ }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/PlannedExerciseStep.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/PlannedExerciseStep.kt
new file mode 100644
index 0000000..a927b31
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/PlannedExerciseStep.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.records
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+
+/**
+ * A single step within an [PlannedExerciseBlock] e.g. 8x 60kg barbell squats.
+ *
+ * @param exerciseType The type of exercise that this step involves.
+ * @param exercisePhase The phase e.g. 'warmup' that this step belongs to.
+ * @param description The description of this step.
+ * @param completionGoal The goal that must be completed to finish this step.
+ * @param performanceTargets Performance related targets that should be met during this step.
+ */
+class PlannedExerciseStep(
+ @property:ExerciseSegment.Companion.ExerciseSegmentTypes val exerciseType: Int,
+ @property:ExercisePhase val exercisePhase: Int,
+ val completionGoal: ExerciseCompletionGoal,
+ val performanceTargets: List<ExercisePerformanceTarget>,
+ val description: String? = null,
+) {
+ companion object {
+ /* Next Id: 6. */
+ /** An unknown phase of exercise. */
+ const val EXERCISE_PHASE_UNKNOWN = 0
+ /** A warmup. */
+ const val EXERCISE_PHASE_WARMUP = 1
+ /** A rest. */
+ const val EXERCISE_PHASE_REST = 2
+ /** Active exercise. */
+ const val EXERCISE_PHASE_ACTIVE = 3
+ /** Cooldown exercise, typically at the end of a workout. */
+ const val EXERCISE_PHASE_COOLDOWN = 4
+ /** Lower intensity, active exercise. */
+ const val EXERCISE_PHASE_RECOVERY = 5
+
+ /** List of supported exercise phase types. */
+ @Retention(AnnotationRetention.SOURCE)
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef(
+ value =
+ [
+ EXERCISE_PHASE_UNKNOWN,
+ EXERCISE_PHASE_WARMUP,
+ EXERCISE_PHASE_REST,
+ EXERCISE_PHASE_ACTIVE,
+ EXERCISE_PHASE_COOLDOWN,
+ EXERCISE_PHASE_RECOVERY,
+ ]
+ )
+ annotation class ExercisePhase
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is PlannedExerciseStep) return false
+
+ if (exerciseType != other.exerciseType) return false
+ if (exercisePhase != other.exercisePhase) return false
+ if (description != other.description) return false
+ if (completionGoal != other.completionGoal) return false
+ if (performanceTargets != other.performanceTargets) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = exerciseType
+ result = 31 * result + exercisePhase
+ result = 31 * result + (description?.hashCode() ?: 0)
+ result = 31 * result + completionGoal.hashCode()
+ result = 31 * result + performanceTargets.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "PlannedExerciseStep(exerciseType=$exerciseType, exerciseCategory=$exercisePhase, description=$description, completionGoal=$completionGoal, performanceTargets=$performanceTargets)"
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseCompletionGoalTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseCompletionGoalTest.kt
new file mode 100644
index 0000000..ab01101
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseCompletionGoalTest.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.records
+
+import androidx.health.connect.client.records.ExerciseCompletionGoal.ActiveCaloriesBurnedGoal
+import androidx.health.connect.client.records.ExerciseCompletionGoal.DistanceAndDurationGoal
+import androidx.health.connect.client.records.ExerciseCompletionGoal.DistanceGoal
+import androidx.health.connect.client.records.ExerciseCompletionGoal.DurationGoal
+import androidx.health.connect.client.records.ExerciseCompletionGoal.ManualCompletion
+import androidx.health.connect.client.records.ExerciseCompletionGoal.RepetitionsGoal
+import androidx.health.connect.client.records.ExerciseCompletionGoal.StepsGoal
+import androidx.health.connect.client.records.ExerciseCompletionGoal.TotalCaloriesBurnedGoal
+import androidx.health.connect.client.records.ExerciseCompletionGoal.UnknownGoal
+import androidx.health.connect.client.units.calories
+import androidx.health.connect.client.units.meters
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ExerciseCompletionGoalTest {
+ @Test
+ fun distanceGoal_hashCodeAndEquals() {
+ val distanceGoal1 = DistanceGoal(20.0.meters)
+ val distanceGoal1Duplicate = DistanceGoal(20.0.meters)
+ val distanceGoal2 = DistanceGoal(30.0.meters)
+
+ assertThat(distanceGoal1.hashCode()).isNotEqualTo(distanceGoal2.hashCode())
+ assertThat(distanceGoal1).isNotEqualTo(distanceGoal2)
+ assertThat(distanceGoal1.hashCode()).isEqualTo(distanceGoal1Duplicate.hashCode())
+ assertThat(distanceGoal1).isEqualTo(distanceGoal1Duplicate)
+ }
+
+ @Test
+ fun distanceWithVariableRestGoal_hashCodeAndEquals() {
+ val distanceAndDurationGoal1 = DistanceAndDurationGoal(20.0.meters, Duration.ofMinutes(2))
+ val distanceAndDurationGoal1Duplicate =
+ DistanceAndDurationGoal(20.0.meters, Duration.ofMinutes(2))
+ val distanceAndDurationGoal2 = DistanceAndDurationGoal(20.0.meters, Duration.ofMinutes(4))
+
+ assertThat(distanceAndDurationGoal1.hashCode())
+ .isNotEqualTo(distanceAndDurationGoal2.hashCode())
+ assertThat(distanceAndDurationGoal1).isNotEqualTo(distanceAndDurationGoal2)
+ assertThat(distanceAndDurationGoal1.hashCode())
+ .isEqualTo(distanceAndDurationGoal1Duplicate.hashCode())
+ assertThat(distanceAndDurationGoal1).isEqualTo(distanceAndDurationGoal1Duplicate)
+ }
+
+ @Test
+ fun stepsGoal_hashCodeAndEquals() {
+ val stepsGoal1 = StepsGoal(1000)
+ val stepsGoal1Duplicate = StepsGoal(1000)
+ val stepsGoal2 = StepsGoal(2000)
+
+ assertThat(stepsGoal1.hashCode()).isNotEqualTo(stepsGoal2.hashCode())
+ assertThat(stepsGoal1).isNotEqualTo(stepsGoal2)
+ assertThat(stepsGoal1.hashCode()).isEqualTo(stepsGoal1Duplicate.hashCode())
+ assertThat(stepsGoal1).isEqualTo(stepsGoal1Duplicate)
+ }
+
+ @Test
+ fun durationGoal_hashCodeAndEquals() {
+ val durationGoal1 = DurationGoal(Duration.ofMinutes(30))
+ val durationGoal1Duplicate = DurationGoal(Duration.ofMinutes(30))
+ val durationGoal2 = DurationGoal(Duration.ofMinutes(60))
+
+ assertThat(durationGoal1.hashCode()).isNotEqualTo(durationGoal2.hashCode())
+ assertThat(durationGoal1).isNotEqualTo(durationGoal2)
+ assertThat(durationGoal1.hashCode()).isEqualTo(durationGoal1Duplicate.hashCode())
+ assertThat(durationGoal1).isEqualTo(durationGoal1Duplicate)
+ }
+
+ @Test
+ fun repetitionsGoal_hashCodeAndEquals() {
+ val repetitionsGoal1 = RepetitionsGoal(Duration.ofSeconds(10))
+ val repetitionsGoal1Duplicate = RepetitionsGoal(Duration.ofSeconds(10))
+ val repetitionsGoal2 = RepetitionsGoal(Duration.ofSeconds(20))
+
+ assertThat(repetitionsGoal1.hashCode()).isNotEqualTo(repetitionsGoal2.hashCode())
+ assertThat(repetitionsGoal1).isNotEqualTo(repetitionsGoal2)
+ assertThat(repetitionsGoal1.hashCode()).isEqualTo(repetitionsGoal1Duplicate.hashCode())
+ assertThat(repetitionsGoal1).isEqualTo(repetitionsGoal1Duplicate)
+ }
+
+ @Test
+ fun totalCaloriesBurnedGoal_hashCodeAndEquals() {
+ val totalCaloriesBurnedGoal1 = TotalCaloriesBurnedGoal(100.calories)
+ val totalCaloriesBurnedGoal1Duplicate = TotalCaloriesBurnedGoal(100.calories)
+ val totalCaloriesBurnedGoal2 = TotalCaloriesBurnedGoal(200.calories)
+
+ assertThat(totalCaloriesBurnedGoal1.hashCode())
+ .isNotEqualTo(totalCaloriesBurnedGoal2.hashCode())
+ assertThat(totalCaloriesBurnedGoal1).isNotEqualTo(totalCaloriesBurnedGoal2)
+ assertThat(totalCaloriesBurnedGoal1.hashCode())
+ .isEqualTo(totalCaloriesBurnedGoal1Duplicate.hashCode())
+ assertThat(totalCaloriesBurnedGoal1).isEqualTo(totalCaloriesBurnedGoal1Duplicate)
+ }
+
+ @Test
+ fun activeCaloriesBurnedGoal_hashCodeAndEquals() {
+ val activeCaloriesBurnedGoal1 = ActiveCaloriesBurnedGoal(100.calories)
+ val activeCaloriesBurnedGoal1Duplicate = ActiveCaloriesBurnedGoal(100.calories)
+ val activeCaloriesBurnedGoal2 = ActiveCaloriesBurnedGoal(200.calories)
+
+ assertThat(activeCaloriesBurnedGoal1.hashCode())
+ .isNotEqualTo(activeCaloriesBurnedGoal2.hashCode())
+ assertThat(activeCaloriesBurnedGoal1).isNotEqualTo(activeCaloriesBurnedGoal2)
+ assertThat(activeCaloriesBurnedGoal1.hashCode())
+ .isEqualTo(activeCaloriesBurnedGoal1Duplicate.hashCode())
+ assertThat(activeCaloriesBurnedGoal1).isEqualTo(activeCaloriesBurnedGoal1Duplicate)
+ }
+
+ @Test
+ fun unknownGoal_hashCodeAndEquals() {
+ assertThat(UnknownGoal.hashCode()).isEqualTo(UnknownGoal.hashCode())
+ assertThat(UnknownGoal).isEqualTo(UnknownGoal)
+ }
+
+ @Test
+ fun unspecifiedGoal_hashCodeAndEquals() {
+ assertThat(ManualCompletion).isEqualTo(ManualCompletion)
+ assertThat(ManualCompletion).isEqualTo(ManualCompletion)
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExercisePerformanceTargetTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExercisePerformanceTargetTest.kt
new file mode 100644
index 0000000..5ac3c62
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExercisePerformanceTargetTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.records
+
+import androidx.health.connect.client.records.ExercisePerformanceTarget.AmrapTarget
+import androidx.health.connect.client.records.ExercisePerformanceTarget.CadenceTarget
+import androidx.health.connect.client.records.ExercisePerformanceTarget.HeartRateTarget
+import androidx.health.connect.client.records.ExercisePerformanceTarget.PowerTarget
+import androidx.health.connect.client.records.ExercisePerformanceTarget.RateOfPerceivedExertionTarget
+import androidx.health.connect.client.records.ExercisePerformanceTarget.SpeedTarget
+import androidx.health.connect.client.records.ExercisePerformanceTarget.UnknownTarget
+import androidx.health.connect.client.records.ExercisePerformanceTarget.WeightTarget
+import androidx.health.connect.client.units.kilograms
+import androidx.health.connect.client.units.metersPerSecond
+import androidx.health.connect.client.units.watts
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ExercisePerformanceTargetTest {
+ @Test
+ fun powerTarget_hashCodeAndEquals() {
+ val target1 = PowerTarget(minPower = 10.watts, maxPower = 20.watts)
+ val target1Copy = PowerTarget(minPower = 10.watts, maxPower = 20.watts)
+ val target2 = PowerTarget(minPower = 15.watts, maxPower = 25.watts)
+
+ assertThat(target1.hashCode()).isNotEqualTo(target2.hashCode())
+ assertThat(target1).isNotEqualTo(target2)
+ assertThat(target1.hashCode()).isEqualTo(target1Copy.hashCode())
+ assertThat(target1).isEqualTo(target1Copy)
+ }
+
+ @Test
+ fun speedTarget_hashCodeAndEquals() {
+ val target1 = SpeedTarget(minSpeed = 10.metersPerSecond, maxSpeed = 20.metersPerSecond)
+ val target1Copy = SpeedTarget(minSpeed = 10.metersPerSecond, maxSpeed = 20.metersPerSecond)
+ val target2 = SpeedTarget(minSpeed = 15.metersPerSecond, maxSpeed = 25.metersPerSecond)
+
+ assertThat(target1.hashCode()).isNotEqualTo(target2.hashCode())
+ assertThat(target1).isNotEqualTo(target2)
+ assertThat(target1.hashCode()).isEqualTo(target1Copy.hashCode())
+ assertThat(target1).isEqualTo(target1Copy)
+ }
+
+ @Test
+ fun cadenceTarget_hashCodeAndEquals() {
+ val target1 = CadenceTarget(minCadence = 10.0, maxCadence = 20.0)
+ val target1Copy = CadenceTarget(minCadence = 10.0, maxCadence = 20.0)
+ val target2 = CadenceTarget(minCadence = 15.0, maxCadence = 25.0)
+
+ assertThat(target1.hashCode()).isNotEqualTo(target2.hashCode())
+ assertThat(target1).isNotEqualTo(target2)
+ assertThat(target1.hashCode()).isEqualTo(target1Copy.hashCode())
+ assertThat(target1).isEqualTo(target1Copy)
+ }
+
+ @Test
+ fun heartRateTarget_hashCodeAndEquals() {
+ val target1 = HeartRateTarget(minHeartRate = 100.0, maxHeartRate = 120.0)
+ val target1Copy = HeartRateTarget(minHeartRate = 100.0, maxHeartRate = 120.0)
+ val target2 = HeartRateTarget(minHeartRate = 110.0, maxHeartRate = 130.0)
+
+ assertThat(target1.hashCode()).isNotEqualTo(target2.hashCode())
+ assertThat(target1).isNotEqualTo(target2)
+ assertThat(target1.hashCode()).isEqualTo(target1Copy.hashCode())
+ assertThat(target1).isEqualTo(target1Copy)
+ }
+
+ @Test
+ fun weightTarget_hashCodeAndEquals() {
+ val target1 = WeightTarget(mass = 100.kilograms)
+ val target1Copy = WeightTarget(mass = 100.kilograms)
+ val target2 = WeightTarget(mass = 120.kilograms)
+
+ assertThat(target1.hashCode()).isNotEqualTo(target2.hashCode())
+ assertThat(target1).isNotEqualTo(target2)
+ assertThat(target1.hashCode()).isEqualTo(target1Copy.hashCode())
+ assertThat(target1).isEqualTo(target1Copy)
+ }
+
+ @Test
+ fun rateOfPerceivedExertionTarget_hashCodeAndEquals() {
+ val target1 = RateOfPerceivedExertionTarget(rpe = 5)
+ val target1Copy = RateOfPerceivedExertionTarget(rpe = 5)
+ val target2 = RateOfPerceivedExertionTarget(rpe = 7)
+
+ assertThat(target1.hashCode()).isNotEqualTo(target2.hashCode())
+ assertThat(target1).isNotEqualTo(target2)
+ assertThat(target1.hashCode()).isEqualTo(target1Copy.hashCode())
+ assertThat(target1).isEqualTo(target1Copy)
+ }
+
+ @Test
+ fun amrapTarget_hashCodeAndEquals() {
+ assertThat(AmrapTarget.hashCode()).isEqualTo(AmrapTarget.hashCode())
+ assertThat(AmrapTarget).isEqualTo(AmrapTarget)
+ }
+
+ @Test
+ fun unknownTarget_hashCodeAndEquals() {
+ assertThat(UnknownTarget.hashCode()).isEqualTo(UnknownTarget.hashCode())
+ assertThat(UnknownTarget).isEqualTo(UnknownTarget)
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
index 49709f7..bf8240c 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
@@ -555,4 +555,21 @@
"ExerciseSessionRecord(startTime=1970-01-01T00:00:01.234Z, startZoneOffset=null, endTime=1970-01-01T00:00:01.236Z, endZoneOffset=null, exerciseType=8, title=title, notes=notes, metadata=Metadata(id='', dataOrigin=DataOrigin(packageName=''), lastModifiedTime=1970-01-01T00:00:00Z, clientRecordId=null, clientRecordVersion=0, device=null, recordingMethod=0), segments=[ExerciseSegment(startTime=1970-01-01T00:00:01.234Z, endTime=1970-01-01T00:00:01.235Z, segmentType=7, repetitions=0)], laps=[ExerciseLap(startTime=1970-01-01T00:00:01.235Z, endTime=1970-01-01T00:00:01.236Z, length=10.0 meters)], exerciseRouteResult=Data(exerciseRoute=ExerciseRoute(route=[Location(time=1970-01-01T00:00:01.234Z, latitude=34.5, longitude=-34.5, horizontalAccuracy=0.4 meters, verticalAccuracy=1.3 meters, altitude=23.4 meters)])))"
)
}
+
+ @Test
+ fun plannedExercise_fieldCanBeOptionallySet() {
+ assertThat(
+ ExerciseSessionRecord(
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(1236L),
+ endZoneOffset = null,
+ exerciseType = EXERCISE_TYPE_BIKING,
+ exerciseRoute = null,
+ plannedExerciseSessionId = "some_id"
+ )
+ .plannedExerciseSessionId
+ )
+ .isEqualTo("some_id")
+ }
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/PlannedExerciseBlockTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/PlannedExerciseBlockTest.kt
new file mode 100644
index 0000000..e2e5dac
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/PlannedExerciseBlockTest.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.records
+
+import androidx.health.connect.client.units.kilometers
+import androidx.health.connect.client.units.meters
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PlannedExerciseBlockTest {
+
+ @Test
+ fun identicalBlocks_bothAreEqual() {
+ assertThat(
+ PlannedExerciseBlock(
+ repetitions = 2,
+ description = "Main set",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
+ PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
+ completionGoal = ExerciseCompletionGoal.DistanceGoal(3.kilometers),
+ performanceTargets = listOf()
+ )
+ )
+ )
+ )
+ .isEqualTo(
+ PlannedExerciseBlock(
+ repetitions = 2,
+ description = "Main set",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
+ PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
+ completionGoal = ExerciseCompletionGoal.DistanceGoal(3.kilometers),
+ performanceTargets = listOf()
+ )
+ )
+ )
+ )
+ }
+
+ @Test
+ fun differentBlocks_notEqual() {
+ assertThat(
+ PlannedExerciseBlock(
+ repetitions = 2,
+ description = "Main set",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
+ PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
+ completionGoal = ExerciseCompletionGoal.DistanceGoal(1.kilometers),
+ performanceTargets = listOf()
+ )
+ )
+ )
+ )
+ .isNotEqualTo(
+ PlannedExerciseBlock(
+ repetitions = 3,
+ description = "Warmup",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING_STATIONARY,
+ PlannedExerciseStep.EXERCISE_PHASE_WARMUP,
+ completionGoal = ExerciseCompletionGoal.DistanceGoal(200.meters),
+ performanceTargets = listOf()
+ )
+ )
+ )
+ )
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/PlannedExerciseSessionRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/PlannedExerciseSessionRecordTest.kt
new file mode 100644
index 0000000..04236b2
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/PlannedExerciseSessionRecordTest.kt
@@ -0,0 +1,457 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.records
+
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Length
+import androidx.health.connect.client.units.Power
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.ZoneId
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PlannedExerciseSessionRecordTest {
+
+ @Test
+ fun identicalRecords_bothAreEqual() {
+ assertThat(
+ PlannedExerciseSessionRecord(
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(1236L),
+ endZoneOffset = null,
+ blocks =
+ listOf(
+ PlannedExerciseBlock(
+ 3,
+ description = "Warmup",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING_STATIONARY,
+ PlannedExerciseStep.EXERCISE_PHASE_WARMUP,
+ completionGoal =
+ ExerciseCompletionGoal.DistanceGoal(
+ Length.meters(200.0)
+ ),
+ performanceTargets =
+ listOf(
+ ExercisePerformanceTarget.PowerTarget(
+ minPower = Power.watts(180.0),
+ maxPower = Power.watts(220.0)
+ )
+ )
+ )
+ )
+ )
+ ),
+ title = "Total Body Conditioning",
+ notes = "A tough workout that mixes both cardio and strength!",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+ )
+ .isEqualTo(
+ PlannedExerciseSessionRecord(
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(1236L),
+ endZoneOffset = null,
+ blocks =
+ listOf(
+ PlannedExerciseBlock(
+ 3,
+ description = "Warmup",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING_STATIONARY,
+ PlannedExerciseStep.EXERCISE_PHASE_WARMUP,
+ completionGoal =
+ ExerciseCompletionGoal.DistanceGoal(
+ Length.meters(200.0)
+ ),
+ performanceTargets =
+ listOf(
+ ExercisePerformanceTarget.PowerTarget(
+ minPower = Power.watts(180.0),
+ maxPower = Power.watts(220.0)
+ )
+ )
+ )
+ )
+ )
+ ),
+ title = "Total Body Conditioning",
+ notes = "A tough workout that mixes both cardio and strength!",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+ )
+ }
+
+ @Test
+ fun differentRecords_notEqual() {
+ assertThat(
+ PlannedExerciseSessionRecord(
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(1236L),
+ endZoneOffset = null,
+ blocks =
+ listOf(
+ PlannedExerciseBlock(
+ 2,
+ description = "Main set",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
+ PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
+ completionGoal =
+ ExerciseCompletionGoal.DistanceGoal(
+ Length.meters(3000.0)
+ ),
+ performanceTargets =
+ listOf(
+ ExercisePerformanceTarget.PowerTarget(
+ minPower = Power.watts(200.0),
+ maxPower = Power.watts(240.0)
+ )
+ )
+ )
+ )
+ )
+ ),
+ title = "Total Body Conditioning",
+ notes = "A tough workout that mixes both cardio and strength!",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+ )
+ .isNotEqualTo(
+ PlannedExerciseSessionRecord(
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(1236L),
+ endZoneOffset = null,
+ blocks =
+ listOf(
+ PlannedExerciseBlock(
+ 3,
+ description = "Warmup",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING_STATIONARY,
+ PlannedExerciseStep.EXERCISE_PHASE_WARMUP,
+ completionGoal =
+ ExerciseCompletionGoal.DistanceGoal(
+ Length.meters(200.0)
+ ),
+ performanceTargets = listOf()
+ )
+ )
+ )
+ ),
+ title = "Total Body Conditioning",
+ notes = "A tough workout that mixes both cardio and strength!",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+ )
+ }
+
+ @Test
+ fun invalidTimes_startAfterEnd_throws() {
+ assertFailsWith<IllegalArgumentException> {
+ PlannedExerciseSessionRecord(
+ startTime = Instant.ofEpochMilli(100L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(50L),
+ endZoneOffset = null,
+ blocks =
+ listOf(
+ PlannedExerciseBlock(
+ 3,
+ description = "Warmup",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING_STATIONARY,
+ PlannedExerciseStep.EXERCISE_PHASE_WARMUP,
+ completionGoal =
+ ExerciseCompletionGoal.DistanceGoal(
+ Length.meters(200.0)
+ ),
+ performanceTargets =
+ listOf(
+ ExercisePerformanceTarget.PowerTarget(
+ minPower = Power.watts(180.0),
+ maxPower = Power.watts(220.0)
+ )
+ )
+ )
+ )
+ )
+ ),
+ title = "Total Body Conditioning Workout",
+ notes = "A tough workout that mixes both cardio and strength!",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+ }
+ }
+
+ @Test
+ fun identicalRecords_localDateConstructor_bothAreEqual() {
+ val startDate = LocalDate.of(2022, 12, 31)
+ val duration = Duration.ofHours(1)
+
+ assertThat(
+ PlannedExerciseSessionRecord(
+ startDate = startDate,
+ duration = duration,
+ blocks =
+ listOf(
+ PlannedExerciseBlock(
+ 3,
+ description = "Warmup",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING_STATIONARY,
+ PlannedExerciseStep.EXERCISE_PHASE_WARMUP,
+ completionGoal =
+ ExerciseCompletionGoal.DistanceGoal(
+ Length.meters(200.0)
+ ),
+ performanceTargets =
+ listOf(
+ ExercisePerformanceTarget.PowerTarget(
+ minPower = Power.watts(180.0),
+ maxPower = Power.watts(220.0)
+ )
+ )
+ )
+ )
+ )
+ ),
+ title = "Total Body Conditioning",
+ notes = "A tough workout that mixes both cardio and strength!",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+ )
+ .isEqualTo(
+ PlannedExerciseSessionRecord(
+ startDate = startDate,
+ duration = duration,
+ blocks =
+ listOf(
+ PlannedExerciseBlock(
+ 3,
+ description = "Warmup",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING_STATIONARY,
+ PlannedExerciseStep.EXERCISE_PHASE_WARMUP,
+ completionGoal =
+ ExerciseCompletionGoal.DistanceGoal(
+ Length.meters(200.0)
+ ),
+ performanceTargets =
+ listOf(
+ ExercisePerformanceTarget.PowerTarget(
+ minPower = Power.watts(180.0),
+ maxPower = Power.watts(220.0)
+ )
+ )
+ )
+ )
+ )
+ ),
+ title = "Total Body Conditioning",
+ notes = "A tough workout that mixes both cardio and strength!",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+ )
+ }
+
+ @Test
+ fun differentRecords_localDateConstructor_notEqual() {
+ val startDate = LocalDate.of(2022, 12, 31)
+ val duration = Duration.ofHours(1)
+
+ assertThat(
+ PlannedExerciseSessionRecord(
+ startDate = startDate,
+ duration = duration,
+ blocks =
+ listOf(
+ PlannedExerciseBlock(
+ 2,
+ description = "Main set",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
+ PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
+ completionGoal =
+ ExerciseCompletionGoal.DistanceGoal(
+ Length.meters(3000.0)
+ ),
+ performanceTargets =
+ listOf(
+ ExercisePerformanceTarget.PowerTarget(
+ minPower = Power.watts(200.0),
+ maxPower = Power.watts(240.0)
+ )
+ )
+ )
+ )
+ )
+ ),
+ title = "",
+ notes = "A tough workout that mixes both cardio and strength!",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+ )
+ .isNotEqualTo(
+ PlannedExerciseSessionRecord(
+ startDate = startDate,
+ duration = duration,
+ blocks =
+ listOf(
+ PlannedExerciseBlock(
+ 3,
+ description = "Warmup",
+ steps =
+ listOf(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING_STATIONARY,
+ PlannedExerciseStep.EXERCISE_PHASE_WARMUP,
+ completionGoal =
+ ExerciseCompletionGoal.DistanceGoal(
+ Length.meters(200.0)
+ ),
+ performanceTargets = listOf()
+ )
+ )
+ )
+ ),
+ title = "Total Body Conditioning Workout",
+ notes = "A tough workout that mixes both cardio and strength!",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+ )
+ }
+
+ @Test
+ fun completedExerciseSessionId_setsCorrectly() {
+ // Note: this can only be set via the internal constructor.
+ val record =
+ PlannedExerciseSessionRecord(
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(1236L),
+ endZoneOffset = null,
+ hasExplicitTime = true,
+ blocks = listOf(),
+ title = "My Planned Session",
+ notes = "Notes",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ completedExerciseSessionId = "some-uuid",
+ metadata = Metadata("record_id", DataOrigin("com.some.app"))
+ )
+ assertThat(record.completedExerciseSessionId).isEqualTo("some-uuid")
+ }
+
+ @Test
+ fun completedExerciseSessionId_defaultsToNull() {
+ val record =
+ PlannedExerciseSessionRecord(
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(1236L),
+ endZoneOffset = null,
+ blocks = listOf(),
+ title = "My Planned Session",
+ notes = "Notes",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS
+ )
+ assertThat(record.completedExerciseSessionId).isNull()
+ }
+
+ @Test
+ fun localDateConstructor_implicitlySetsStartAndEndTime() {
+ val startDate = LocalDate.of(2023, 10, 26)
+ val record =
+ PlannedExerciseSessionRecord(
+ startDate = startDate,
+ duration = Duration.ofHours(1),
+ blocks = listOf(),
+ title = "My Planned Session",
+ notes = "Notes",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+
+ assertThat(record.startTime)
+ .isEqualTo(startDate.atTime(LocalTime.NOON).atZone(ZoneId.systemDefault()).toInstant())
+ assertThat(record.endTime)
+ .isEqualTo(
+ startDate
+ .atTime(LocalTime.NOON)
+ .atZone(ZoneId.systemDefault())
+ .toInstant()
+ .plus(Duration.ofHours(1))
+ )
+ }
+
+ @Test
+ fun localDateConstructor_hasExplicitTimeIsFalse() {
+ val record =
+ PlannedExerciseSessionRecord(
+ startDate = LocalDate.now(),
+ duration = Duration.ofMinutes(30),
+ blocks = listOf(),
+ title = "My Planned Session",
+ notes = "Notes",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+ assertThat(record.hasExplicitTime).isFalse()
+ }
+
+ @Test
+ fun instantConstructor_hasExplicitTimeIsTrue() {
+ val record =
+ PlannedExerciseSessionRecord(
+ startTime = Instant.now(),
+ startZoneOffset = null,
+ endTime = Instant.now().plusSeconds(1800),
+ endZoneOffset = null,
+ blocks = listOf(),
+ title = "My Planned Session",
+ notes = "Notes",
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS,
+ )
+ assertThat(record.hasExplicitTime).isTrue()
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/PlannedExerciseStepTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/PlannedExerciseStepTest.kt
new file mode 100644
index 0000000..e24aa3d
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/PlannedExerciseStepTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.records
+
+import androidx.health.connect.client.units.kilometers
+import androidx.health.connect.client.units.meters
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PlannedExerciseStepTest {
+
+ @Test
+ fun identicalSteps_bothAreEqual() {
+ assertThat(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
+ PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
+ ExerciseCompletionGoal.DistanceGoal(1.kilometers),
+ listOf(),
+ "Run fast for 1km",
+ )
+ )
+ .isEqualTo(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
+ PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
+ ExerciseCompletionGoal.DistanceGoal(1.kilometers),
+ listOf(),
+ "Run fast for 1km",
+ )
+ )
+ }
+
+ @Test
+ fun differentSteps_notEqual() {
+ assertThat(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
+ PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
+ ExerciseCompletionGoal.DistanceGoal(1.kilometers),
+ listOf(),
+ "Run fast for 1km",
+ )
+ )
+ .isNotEqualTo(
+ PlannedExerciseStep(
+ ExerciseSegment.EXERCISE_SEGMENT_TYPE_BIKING_STATIONARY,
+ PlannedExerciseStep.EXERCISE_PHASE_WARMUP,
+ ExerciseCompletionGoal.DistanceGoal(200.meters),
+ listOf(),
+ "Warmup",
+ )
+ )
+ }
+}
diff --git a/libraryversions.toml b/libraryversions.toml
index 7125ef8..29d563e 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -12,10 +12,10 @@
BLUETOOTH = "1.0.0-alpha02"
BROWSER = "1.9.0-alpha01"
BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.5.0-alpha01"
+CAMERA = "1.5.0-alpha02"
CAMERA_PIPE = "1.0.0-alpha01"
CAMERA_TESTING = "1.0.0-alpha01"
-CAMERA_VIEWFINDER = "1.4.0-alpha08"
+CAMERA_VIEWFINDER = "1.4.0-alpha09"
CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.7.0-beta02"
COLLECTION = "1.5.0-alpha02"
@@ -107,7 +107,7 @@
PRIVACYSANDBOX_ADS = "1.1.0-beta10"
PRIVACYSANDBOX_PLUGINS = "1.0.0-alpha03"
PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha14"
-PRIVACYSANDBOX_TOOLS = "1.0.0-alpha09"
+PRIVACYSANDBOX_TOOLS = "1.0.0-alpha10"
PRIVACYSANDBOX_UI = "1.0.0-alpha10"
PROFILEINSTALLER = "1.4.0-rc01"
RECOMMENDATION = "1.1.0-alpha01"
diff --git a/lifecycle/lifecycle-runtime/api/current.txt b/lifecycle/lifecycle-runtime/api/current.txt
index 4ba40ea..d0ae223 100644
--- a/lifecycle/lifecycle-runtime/api/current.txt
+++ b/lifecycle/lifecycle-runtime/api/current.txt
@@ -11,13 +11,13 @@
public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner provider);
- method public void addObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @MainThread public void addObserver(androidx.lifecycle.LifecycleObserver observer);
method @VisibleForTesting public static final androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
method public androidx.lifecycle.Lifecycle.State getCurrentState();
method public int getObserverCount();
method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State state);
- method public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @MainThread public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
property public androidx.lifecycle.Lifecycle.State currentState;
property public kotlinx.coroutines.flow.StateFlow<androidx.lifecycle.Lifecycle.State> currentStateFlow;
diff --git a/lifecycle/lifecycle-runtime/api/restricted_current.txt b/lifecycle/lifecycle-runtime/api/restricted_current.txt
index 1f60a847..f925edd 100644
--- a/lifecycle/lifecycle-runtime/api/restricted_current.txt
+++ b/lifecycle/lifecycle-runtime/api/restricted_current.txt
@@ -11,13 +11,13 @@
public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner provider);
- method public void addObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @MainThread public void addObserver(androidx.lifecycle.LifecycleObserver observer);
method @VisibleForTesting public static final androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
method public androidx.lifecycle.Lifecycle.State getCurrentState();
method public int getObserverCount();
method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State state);
- method public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @MainThread public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
property public androidx.lifecycle.Lifecycle.State currentState;
property public kotlinx.coroutines.flow.StateFlow<androidx.lifecycle.Lifecycle.State> currentStateFlow;
diff --git a/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/LifecycleRegistry.kt b/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/LifecycleRegistry.kt
index 0971d24..6b4de36 100644
--- a/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/LifecycleRegistry.kt
+++ b/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/LifecycleRegistry.kt
@@ -15,6 +15,7 @@
*/
package androidx.lifecycle
+import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
import kotlin.jvm.JvmStatic
@@ -37,6 +38,10 @@
constructor(provider: LifecycleOwner) : Lifecycle {
override var currentState: State
+ @MainThread() override fun addObserver(observer: LifecycleObserver)
+
+ @MainThread() override fun removeObserver(observer: LifecycleObserver)
+
/**
* Sets the current state and notifies the observers.
*
diff --git a/lifecycle/lifecycle-runtime/src/jvmCommonMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt b/lifecycle/lifecycle-runtime/src/jvmCommonMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
index 5342565..38f21ec 100644
--- a/lifecycle/lifecycle-runtime/src/jvmCommonMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
+++ b/lifecycle/lifecycle-runtime/src/jvmCommonMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
@@ -169,7 +169,8 @@
* @param observer The observer to notify.
* @throws IllegalStateException if no event up from observer's initial state
*/
- override fun addObserver(observer: LifecycleObserver) {
+ @MainThread
+ actual override fun addObserver(observer: LifecycleObserver) {
enforceMainThreadIfNeeded("addObserver")
val initialState = if (state == State.DESTROYED) State.DESTROYED else State.INITIALIZED
val statefulObserver = ObserverWithState(observer, initialState)
@@ -209,7 +210,8 @@
parentStates.add(state)
}
- override fun removeObserver(observer: LifecycleObserver) {
+ @MainThread
+ actual override fun removeObserver(observer: LifecycleObserver) {
enforceMainThreadIfNeeded("removeObserver")
// we consciously decided not to send destruction events here in opposition to addObserver.
// Our reasons for that:
diff --git a/lifecycle/lifecycle-runtime/src/nonJvmCommonMain/kotlin/androidx/lifecycle/LifecycleRegistry.nonJvm.kt b/lifecycle/lifecycle-runtime/src/nonJvmCommonMain/kotlin/androidx/lifecycle/LifecycleRegistry.nonJvm.kt
index e509628..bb30846 100644
--- a/lifecycle/lifecycle-runtime/src/nonJvmCommonMain/kotlin/androidx/lifecycle/LifecycleRegistry.nonJvm.kt
+++ b/lifecycle/lifecycle-runtime/src/nonJvmCommonMain/kotlin/androidx/lifecycle/LifecycleRegistry.nonJvm.kt
@@ -15,6 +15,7 @@
*/
package androidx.lifecycle
+import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -155,7 +156,8 @@
* @param observer The observer to notify.
* @throws IllegalStateException if no event up from observer's initial state
*/
- override fun addObserver(observer: LifecycleObserver) {
+ @MainThread
+ actual override fun addObserver(observer: LifecycleObserver) {
enforceMainThreadIfNeeded("addObserver")
val initialState = if (state == State.DESTROYED) State.DESTROYED else State.INITIALIZED
val statefulObserver = ObserverWithState(observer, initialState)
@@ -195,7 +197,8 @@
parentStates.add(state)
}
- override fun removeObserver(observer: LifecycleObserver) {
+ @MainThread
+ actual override fun removeObserver(observer: LifecycleObserver) {
enforceMainThreadIfNeeded("removeObserver")
// we consciously decided not to send destruction events here in opposition to addObserver.
// Our reasons for that:
diff --git a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
index 4799f00..b015d75 100644
--- a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
@@ -87,6 +87,7 @@
JSpecifyNullnessMigration.ISSUE,
TypeMirrorToString.ISSUE,
BanNullMarked.ISSUE,
+ AutoValueNullnessOverride.ISSUE,
)
}
}
diff --git a/lint-checks/src/main/java/androidx/build/lint/AutoValueNullnessOverride.kt b/lint-checks/src/main/java/androidx/build/lint/AutoValueNullnessOverride.kt
new file mode 100644
index 0000000..4fca66f
--- /dev/null
+++ b/lint-checks/src/main/java/androidx/build/lint/AutoValueNullnessOverride.kt
@@ -0,0 +1,147 @@
+/*
+ * 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.build.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Incident
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LocationType
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.getUMethod
+import com.android.tools.lint.detector.api.isJava
+import com.android.tools.lint.model.LintModelMavenName
+import com.intellij.psi.PsiMember
+import com.intellij.psi.PsiMethod
+import java.util.EnumSet
+import org.jetbrains.uast.UClass
+
+/**
+ * Enforces that the workaround for b/237064488 is applied, see issue definition for more detail.
+ */
+class AutoValueNullnessOverride : Detector(), Detector.UastScanner {
+ override fun getApplicableUastTypes() = listOf(UClass::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler {
+ return ClassChecker(context)
+ }
+
+ private inner class ClassChecker(val context: JavaContext) : UElementHandler() {
+ override fun visitClass(node: UClass) {
+ if (!node.hasAnnotation("com.google.auto.value.AutoValue")) return
+
+ val classCoordinates = context.findMavenCoordinate(node.javaPsi)
+
+ // Narrow to the relevant methods
+ val missingOverrides =
+ node.allMethods.filter {
+ // Abstract getters are the ones used by the autovalue for builder generation
+ it.isAbstractGetter() &&
+ it.isNullable() &&
+ node.isSuperMethodWithoutOverride(it) &&
+ isFromDifferentCompilation(it, classCoordinates)
+ }
+
+ if (missingOverrides.isEmpty()) return
+
+ // Add overrides that are just copies of the parent source code
+ val insertionText =
+ missingOverrides
+ .mapNotNull {
+ it.getUMethod()?.asSourceString()?.let { parentMethod ->
+ "\n@Override\n$parentMethod"
+ }
+ }
+ .joinToString("\n")
+ val fix =
+ if (isJava(node.language) && insertionText.isNotBlank()) {
+ fix()
+ .replace()
+ // Find the opening of the class body and insert after that
+ .pattern("\\{()")
+ .with(insertionText)
+ .reformat(true)
+ .shortenNames()
+ .range(context.getLocation(node, LocationType.ALL))
+ .build()
+ } else {
+ null
+ }
+
+ val methodNames = missingOverrides.joinToString(", ") { "${it.name}()" }
+ val incident =
+ Incident(context)
+ .issue(ISSUE)
+ .message("Methods need @Nullable overrides for AutoValue: $methodNames")
+ .location(context.getNameLocation(node))
+ .fix(fix)
+
+ context.report(incident)
+ }
+
+ private fun PsiMethod.isAbstractGetter() =
+ parameterList.isEmpty && modifierList.hasModifierProperty("abstract")
+
+ /** Checks if the method return type uses the JSpecify @Nullable. */
+ private fun PsiMethod.isNullable() =
+ returnType?.hasAnnotation("org.jspecify.annotations.Nullable") == true
+
+ /**
+ * Checks that the method is defined in a different class and that the method is not also
+ * defined by a lower class in the hierarchy.
+ */
+ private fun UClass.isSuperMethodWithoutOverride(method: PsiMethod) =
+ method.containingClass?.qualifiedName != qualifiedName &&
+ // This searches starting with the class and then goes to the parent. So if it finds
+ // a matching method that isn't this method, there's an override lower down.
+ findMethodBySignature(method, true) == method
+
+ /**
+ * Checks if [member] has different maven coordinates than the [reference] coordinates, or
+ * if this is in a test context. Tests are in a different compilation from the main source
+ * but have the same maven coordinates, so to be safe always flag them.
+ */
+ private fun isFromDifferentCompilation(member: PsiMember, reference: LintModelMavenName?) =
+ context.findMavenCoordinate(member.containingClass!!) != reference ||
+ context.isTestSource
+ }
+
+ companion object {
+ val ISSUE =
+ Issue.create(
+ "AutoValueNullnessOverride",
+ "AutoValue classes must override @Nullable methods inherited from other projects",
+ """
+ Due to a javac bug in JDK 21 and lower, AutoValue cannot see type-use nullness
+ annotations from other compilations. @AutoValue classes that inherit @Nullable
+ methods must provide an override so the AutoValue compiler doesn't make the
+ value non-null. See b/237064488 for more information.
+ """,
+ Category.CORRECTNESS,
+ 5,
+ Severity.ERROR,
+ Implementation(
+ AutoValueNullnessOverride::class.java,
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+ )
+ )
+ }
+}
diff --git a/lint-checks/src/main/java/androidx/build/lint/LintUtils.kt b/lint-checks/src/main/java/androidx/build/lint/LintUtils.kt
index 7b48cea..54788dd 100644
--- a/lint-checks/src/main/java/androidx/build/lint/LintUtils.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/LintUtils.kt
@@ -16,9 +16,15 @@
package androidx.build.lint
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.model.DefaultLintModelAndroidLibrary
+import com.android.tools.lint.model.DefaultLintModelJavaLibrary
+import com.android.tools.lint.model.LintModelLibrary
+import com.android.tools.lint.model.LintModelMavenName
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMember
import org.jetbrains.kotlin.asJava.namedUnwrappedElement
+import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.kotlin.psi.KtNamedDeclaration
/*
@@ -35,3 +41,77 @@
is KtNamedDeclaration -> element.fqName.toString()
else -> null
}
+
+/** Attempts to find the Maven coordinate for the library containing [member]. */
+internal fun JavaContext.findMavenCoordinate(member: PsiMember): LintModelMavenName? {
+ val mavenName =
+ evaluator.getLibrary(member) ?: evaluator.getProject(member)?.mavenCoordinate ?: return null
+
+ // If the lint model is missing a Maven coordinate for this class, try to infer one from the
+ // JAR's owner library. If we fail, return the broken Maven name anyway.
+ if (mavenName == LintModelMavenName.NONE) {
+ return evaluator
+ .findJarPath(member)
+ ?.let { jarPath ->
+ evaluator.findOwnerLibrary(jarPath.replace('/', File.separatorChar))
+ }
+ ?.getMavenNameFromIdentifier() ?: mavenName
+ }
+
+ // If the lint model says the class lives in a "local AAR", try a little bit harder to match
+ // that to an artifact in a real library based on build directory containment.
+ if (mavenName.groupId == "__local_aars__") {
+ val artifactPath = mavenName.artifactId
+
+ // The artifact is being repackaged within this project. Assume that means it's in the same
+ // Maven group.
+ if (artifactPath.startsWith(project.buildModule.buildFolder.path)) {
+ return project.mavenCoordinate
+ }
+
+ val lastIndexOfBuild = artifactPath.lastIndexOf("/build/")
+ if (lastIndexOfBuild < 0) return null
+
+ // Otherwise, try to find a dependency with a matching path and use its Maven group.
+ val path = artifactPath.substring(0, lastIndexOfBuild)
+ return evaluator.dependencies?.getAll()?.findMavenNameWithJarFileInPath(path, mavenName)
+ ?: mavenName
+ }
+
+ return mavenName
+}
+
+/**
+ * Attempts to find the Maven name for the library with at least one JAR file matching the [path].
+ */
+internal fun List<LintModelLibrary>.findMavenNameWithJarFileInPath(
+ path: String,
+ excludeMavenName: LintModelMavenName? = null
+): LintModelMavenName? {
+ return firstNotNullOfOrNull { library ->
+ val resolvedCoordinates =
+ when {
+ library is DefaultLintModelJavaLibrary -> library.resolvedCoordinates
+ library is DefaultLintModelAndroidLibrary -> library.resolvedCoordinates
+ else -> null
+ }
+
+ if (resolvedCoordinates == null || resolvedCoordinates == excludeMavenName) {
+ return@firstNotNullOfOrNull null
+ }
+
+ val hasMatchingJarFile =
+ when {
+ library == excludeMavenName -> emptyList()
+ library is DefaultLintModelJavaLibrary -> library.jarFiles
+ library is DefaultLintModelAndroidLibrary -> library.jarFiles
+ else -> emptyList()
+ }.any { jarFile -> jarFile.path.startsWith(path) }
+
+ if (hasMatchingJarFile) {
+ return@firstNotNullOfOrNull resolvedCoordinates
+ }
+
+ return@firstNotNullOfOrNull null
+ }
+}
diff --git a/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt b/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
index 700e74d..8fa303c 100644
--- a/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
@@ -30,8 +30,6 @@
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.isKotlin
-import com.android.tools.lint.model.DefaultLintModelAndroidLibrary
-import com.android.tools.lint.model.DefaultLintModelJavaLibrary
import com.android.tools.lint.model.DefaultLintModelMavenName
import com.android.tools.lint.model.LintModelLibrary
import com.android.tools.lint.model.LintModelMavenName
@@ -43,7 +41,6 @@
import com.intellij.psi.PsiMethod
import com.intellij.psi.impl.compiled.ClsAnnotationImpl
import com.intellij.psi.util.PsiTypesUtil
-import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UCallExpression
@@ -504,80 +501,6 @@
}
}
-/** Attempts to find the Maven coordinate for the library containing [member]. */
-private fun JavaContext.findMavenCoordinate(member: PsiMember): LintModelMavenName? {
- val mavenName =
- evaluator.getLibrary(member) ?: evaluator.getProject(member)?.mavenCoordinate ?: return null
-
- // If the lint model is missing a Maven coordinate for this class, try to infer one from the
- // JAR's owner library. If we fail, return the broken Maven name anyway.
- if (mavenName == LintModelMavenName.NONE) {
- return evaluator
- .findJarPath(member)
- ?.let { jarPath ->
- evaluator.findOwnerLibrary(jarPath.replace('/', File.separatorChar))
- }
- ?.getMavenNameFromIdentifier() ?: mavenName
- }
-
- // If the lint model says the class lives in a "local AAR", try a little bit harder to match
- // that to an artifact in a real library based on build directory containment.
- if (mavenName.groupId == "__local_aars__") {
- val artifactPath = mavenName.artifactId
-
- // The artifact is being repackaged within this project. Assume that means it's in the same
- // Maven group.
- if (artifactPath.startsWith(project.buildModule.buildFolder.path)) {
- return project.mavenCoordinate
- }
-
- val lastIndexOfBuild = artifactPath.lastIndexOf("/build/")
- if (lastIndexOfBuild < 0) return null
-
- // Otherwise, try to find a dependency with a matching path and use its Maven group.
- val path = artifactPath.substring(0, lastIndexOfBuild)
- return evaluator.dependencies?.getAll()?.findMavenNameWithJarFileInPath(path, mavenName)
- ?: mavenName
- }
-
- return mavenName
-}
-
-/**
- * Attempts to find the Maven name for the library with at least one JAR file matching the [path].
- */
-internal fun List<LintModelLibrary>.findMavenNameWithJarFileInPath(
- path: String,
- excludeMavenName: LintModelMavenName? = null
-): LintModelMavenName? {
- return firstNotNullOfOrNull { library ->
- val resolvedCoordinates =
- when {
- library is DefaultLintModelJavaLibrary -> library.resolvedCoordinates
- library is DefaultLintModelAndroidLibrary -> library.resolvedCoordinates
- else -> null
- }
-
- if (resolvedCoordinates == null || resolvedCoordinates == excludeMavenName) {
- return@firstNotNullOfOrNull null
- }
-
- val hasMatchingJarFile =
- when {
- library == excludeMavenName -> emptyList()
- library is DefaultLintModelJavaLibrary -> library.jarFiles
- library is DefaultLintModelAndroidLibrary -> library.jarFiles
- else -> emptyList()
- }.any { jarFile -> jarFile.path.startsWith(path) }
-
- if (hasMatchingJarFile) {
- return@firstNotNullOfOrNull resolvedCoordinates
- }
-
- return@firstNotNullOfOrNull null
- }
-}
-
/** Attempts to parse an unversioned Maven name from the library identifier. */
internal fun LintModelLibrary.getMavenNameFromIdentifier(): LintModelMavenName? {
val indexOfSentinel = identifier.indexOf(":@@:")
diff --git a/lint-checks/src/test/java/androidx/build/lint/AutoValueNullnessOverrideTest.kt b/lint-checks/src/test/java/androidx/build/lint/AutoValueNullnessOverrideTest.kt
new file mode 100644
index 0000000..32cc1f9
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/AutoValueNullnessOverrideTest.kt
@@ -0,0 +1,198 @@
+/*
+ * 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.build.lint
+
+import com.android.tools.lint.checks.infrastructure.ProjectDescription
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class AutoValueNullnessOverrideTest :
+ AbstractLintDetectorTest(
+ useDetector = AutoValueNullnessOverride(),
+ useIssues = listOf(AutoValueNullnessOverride.ISSUE),
+ stubs = arrayOf(autovalueStub, jspecifyNonNullStub, jspecifyNullableStub)
+ ) {
+ @Test
+ fun `No superclass`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import com.google.auto.value.AutoValue;
+ import org.jspecify.annotations.Nullable;
+ @AutoValue
+ public abstract class Foo {
+ public abstract @Nullable String getString();
+ }
+ """
+ .trimIndent()
+ )
+ check(input).expectClean()
+ }
+
+ @Test
+ fun `Superclass in same library`() {
+ val input =
+ arrayOf(
+ java(
+ """
+ package test.pkg;
+ import com.google.auto.value.AutoValue;
+ @AutoValue
+ public abstract class Foo extends ParentClass {
+ }
+ """
+ .trimIndent()
+ ),
+ java(
+ """
+ package test.pkg;
+ import org.jspecify.annotations.Nullable;
+ public abstract class ParentClass {
+ public abstract @Nullable String getString();
+ }
+ """
+ .trimIndent()
+ )
+ )
+ check(*input).expectClean()
+ }
+
+ @Test
+ fun `Superclass in different library`() {
+ // Files needs to be set up in project structure for the lint to understand they are from
+ // different libraries
+ val jspecify =
+ project()
+ .files(jspecifyNonNullStub, jspecifyNullableStub)
+ .type(ProjectDescription.Type.LIBRARY)
+
+ val autovalue = project().files(autovalueStub).type(ProjectDescription.Type.LIBRARY)
+
+ val parentLibrary =
+ project()
+ .files(
+ java(
+ """
+ package androidx.example;
+ import org.jspecify.annotations.NonNull;
+ import org.jspecify.annotations.Nullable;
+ public abstract class SuperClass {
+ public abstract @Nullable String getNullableStringNotOverridden();
+ public abstract @NonNull String getNonNullStringNotOverridden();
+ public abstract String getUnannotatedStringNotOverridden();
+
+ public abstract @Nullable String getNullableStringOverridden();
+
+ public abstract @Nullable String getNullableStringOverrideNotAbstract();
+ }
+ """
+ .trimIndent(),
+ ),
+ gradle(
+ """
+ apply plugin: 'com.android.library'
+ group=androidx.example
+ """
+ )
+ )
+ .dependsOn(jspecify)
+ .type(ProjectDescription.Type.LIBRARY)
+
+ val sourceProject =
+ project()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import com.google.auto.value.AutoValue;
+ import androidx.example.SuperClass;
+ @AutoValue
+ public abstract class Foo extends SuperClass {
+ @Override
+ public abstract @Nullable String getNullableStringOverridden();
+
+ @Override
+ public abstract @Nullable String getNullableStringOverrideNotAbstract() {
+ return null;
+ }
+ }
+ """
+ .trimIndent(),
+ ),
+ gradle(
+ """
+ apply plugin: 'com.android.library'
+ group=test.pkg
+ """
+ )
+ )
+ .dependsOn(jspecify)
+ .dependsOn(autovalue)
+ .dependsOn(parentLibrary)
+
+ val expected =
+ """
+ src/main/java/test/pkg/Foo.java:5: Error: Methods need @Nullable overrides for AutoValue: getNullableStringNotOverridden() [AutoValueNullnessOverride]
+ public abstract class Foo extends SuperClass {
+ ~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ val expectedFixDiffs =
+ """
+ Fix for src/main/java/test/pkg/Foo.java line 5: Replace with ...:
+ @@ -6 +6
+ + @Override
+ + public abstract @Nullable String getNullableStringNotOverridden();
+ """
+ .trimIndent()
+
+ lint().projects(sourceProject).run().expect(expected).expectFixDiffs(expectedFixDiffs)
+ }
+
+ companion object {
+ private val autovalueStub =
+ kotlin(
+ """
+ package com.google.auto.value
+ annotation class AutoValue
+ """
+ .trimIndent()
+ )
+ private val jspecifyNullableStub =
+ kotlin(
+ """
+ package org.jspecify.annotations
+ @Target(AnnotationTarget.TYPE)
+ annotation class Nullable
+ """
+ .trimIndent()
+ )
+ private val jspecifyNonNullStub =
+ kotlin(
+ """
+ package org.jspecify.annotations
+ @Target(AnnotationTarget.TYPE)
+ annotation class NonNull
+ """
+ .trimIndent()
+ )
+ }
+}
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index b8d85ac..454de2a 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -31,7 +31,9 @@
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
+import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.pdf.R
import androidx.pdf.ViewState
@@ -153,6 +155,13 @@
private var isSearchMenuAdjusted = false
/**
+ * Specify whether [documentUri] is updated before fragment went in STARTED state.
+ *
+ * If true, we'll trigger a loadFile() operation as soon as fragment reaches STARTED state.
+ */
+ private var pendingDocumentLoad: Boolean = false
+
+ /**
* The URI of the PDF document to display defaulting to `null`.
*
* When this property is set, the fragment begins loading the PDF document. A visual indicator
@@ -160,7 +169,7 @@
* [onLoadDocumentSuccess] callback is invoked. If an error occurs during the loading phase, the
* [onLoadDocumentError] callback is invoked with the exception.
*
- * <p>Note: This property should only be set when the fragment is in the started state.
+ * <p>Note: This property is recommended to be set when the fragment is in the started state.
*/
public var documentUri: Uri? = null
set(value) {
@@ -185,14 +194,20 @@
* is enabled. Deactivating text search mode hides the search menu, clears search results, and
* removes any search-related highlights.
*
- * <p>Note: This property should only be set once the [documentUri] is set.
+ * <p>Note: This property should only be set once fragment is in the started state. Updating it
+ * before will trigger an [IllegalStateException] which will be delivered through
+ * [onLoadDocumentError] to host.
*/
public var isTextSearchActive: Boolean = false
set(value) {
- Preconditions.checkNotNull(
- documentUri,
- "Property can be only be toggled if URI is set already!"
- )
+ if (!isFileRestoring && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
+ onLoadDocumentError(
+ IllegalStateException(
+ "Property can only be toggled after fragment's STARTED state"
+ )
+ )
+ return
+ }
field = value
// Clear selection
@@ -341,6 +356,23 @@
}
}
}
+
+ loadPendingDocumentIfRequired()
+ }
+
+ private fun loadPendingDocumentIfRequired() {
+ lifecycle.addObserver(
+ object : DefaultLifecycleObserver {
+ override fun onStart(owner: LifecycleOwner) {
+ super.onStart(owner)
+ // Check if we're pending on loading a document
+ if (pendingDocumentLoad) {
+ // Trigger load file
+ documentUri?.let { loadFile(it) }
+ }
+ }
+ }
+ )
}
override fun onStart() {
@@ -622,6 +654,7 @@
/** Restores the contents of this Viewer when it is automatically restored by android. */
private fun restoreContents(savedState: Bundle?) {
+ pendingDocumentLoad = savedState?.getBoolean(KEY_PENDING_DOCUMENT_LOAD) ?: false
val dataBundle = savedState?.getBundle(KEY_DATA)
if (dataBundle != null) {
try {
@@ -728,16 +761,19 @@
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
- outState.putBundle(KEY_DATA, fileData?.asBundle())
- layoutHandler?.let { outState.putInt(KEY_LAYOUT_REACH, it.pageLayoutReach) }
- outState.putBoolean(KEY_SHOW_ANNOTATION, isAnnotationIntentResolvable)
- pdfLoaderCallbacks?.selectionModel?.let {
- outState.putParcelable(KEY_PAGE_SELECTION, it.selection().get())
+ outState.apply {
+ putBundle(KEY_DATA, fileData?.asBundle())
+ layoutHandler?.let { putInt(KEY_LAYOUT_REACH, it.pageLayoutReach) }
+ putBoolean(KEY_SHOW_ANNOTATION, isAnnotationIntentResolvable)
+ pdfLoaderCallbacks?.selectionModel?.let {
+ putParcelable(KEY_PAGE_SELECTION, it.selection().get())
+ }
+ putBoolean(
+ KEY_ANNOTATION_BUTTON_VISIBILITY,
+ (annotationButton?.visibility == View.VISIBLE)
+ )
+ putBoolean(KEY_PENDING_DOCUMENT_LOAD, pendingDocumentLoad)
}
- outState.putBoolean(
- KEY_ANNOTATION_BUTTON_VISIBILITY,
- (annotationButton?.visibility == View.VISIBLE)
- )
}
private fun showLoadingErrorView(error: Throwable) {
@@ -748,11 +784,15 @@
}
private fun loadFile(fileUri: Uri) {
- Preconditions.checkNotNull(fileUri)
- Preconditions.checkArgument(
- lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED),
- "Cannot load the URI until the fragment has reached least the STARTED state!"
- )
+ // Early return if fragment is not in STARTED state
+ if (!lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
+ // Update state to mark an early return
+ pendingDocumentLoad = true
+ return
+ }
+ // Update state as loadFile is triggered after in-or-after STARTED state
+ pendingDocumentLoad = false
+
arguments =
Bundle().apply {
putParcelable(KEY_DOCUMENT_URI, fileUri)
@@ -872,5 +912,6 @@
private const val KEY_PAGE_SELECTION: String = "currentPageSelection"
private const val KEY_DOCUMENT_URI: String = "documentUri"
private const val KEY_ANNOTATION_BUTTON_VISIBILITY = "isAnnotationVisible"
+ private const val KEY_PENDING_DOCUMENT_LOAD = "pendingDocumentLoad"
}
}
diff --git a/room/room-gradle-plugin/src/main/java/androidx/room/gradle/integration/AndroidPluginIntegration.kt b/room/room-gradle-plugin/src/main/java/androidx/room/gradle/integration/AndroidPluginIntegration.kt
index 8d6691c..3b609ba2 100644
--- a/room/room-gradle-plugin/src/main/java/androidx/room/gradle/integration/AndroidPluginIntegration.kt
+++ b/room/room-gradle-plugin/src/main/java/androidx/room/gradle/integration/AndroidPluginIntegration.kt
@@ -25,9 +25,7 @@
import androidx.room.gradle.toOptions
import com.android.build.api.AndroidPluginVersion
import com.android.build.api.variant.AndroidComponentsExtension
-import com.android.build.api.variant.AndroidTest
import com.android.build.api.variant.ComponentIdentity
-import com.android.build.api.variant.HasAndroidTest
import com.android.build.api.variant.HasUnitTest
import com.google.devtools.ksp.gradle.KspTaskJvm
import org.gradle.api.Project
@@ -61,7 +59,8 @@
(variant as? HasUnitTest)?.unitTest?.let {
configureAndroidVariant(project, roomExtension, it)
}
- (variant as? HasAndroidTest)?.androidTest?.let {
+ @Suppress("DEPRECATION") // usage of HasAndroidTest
+ (variant as? com.android.build.api.variant.HasAndroidTest)?.androidTest?.let {
configureAndroidVariant(project, roomExtension, it)
}
}
@@ -105,7 +104,8 @@
// Wires a task that will copy schemas from user configured location to the AGP
// generated directory to be used as assets inputs of an Android Test app, enabling
// MigrationTestHelper to automatically pick them up.
- if (variant is AndroidTest) {
+ @Suppress("DEPRECATION") // Usage of AndroidTest
+ if (variant is com.android.build.api.variant.AndroidTest) {
variant.sources.assets?.addGeneratedSourceDirectory(
project.tasks.register(
"copyRoomSchemasToAndroidTestAssets${variant.name.capitalize()}",
diff --git a/savedstate/savedstate/api/current.txt b/savedstate/savedstate/api/current.txt
index 1e9d3e6..7378076 100644
--- a/savedstate/savedstate/api/current.txt
+++ b/savedstate/savedstate/api/current.txt
@@ -1,6 +1,44 @@
// Signature format: 4.0
package androidx.savedstate {
+ public final class SavedStateKt {
+ method public static inline <T> T read(androidx.savedstate.SavedState, kotlin.jvm.functions.Function1<? super androidx.savedstate.SavedStateReader,? extends T> block);
+ method public static inline <T> T read(androidx.savedstate.SavedState, kotlin.jvm.functions.Function1<? super androidx.savedstate.SavedStateReader,? extends T> block);
+ method public static androidx.savedstate.SavedState reader(androidx.savedstate.SavedState);
+ method public static inline <T> T write(androidx.savedstate.SavedState, kotlin.jvm.functions.Function1<? super androidx.savedstate.SavedStateWriter,? extends T> block);
+ method public static inline <T> T write(androidx.savedstate.SavedState, kotlin.jvm.functions.Function1<? super androidx.savedstate.SavedStateWriter,? extends T> block);
+ method public static androidx.savedstate.SavedState writer(androidx.savedstate.SavedState);
+ }
+
+ @kotlin.jvm.JvmInline public final value class SavedStateReader {
+ ctor public SavedStateReader(android.os.Bundle source);
+ method public inline operator boolean contains(String key);
+ method public inline boolean getBoolean(String key);
+ method public inline boolean getBooleanOrElse(String key, kotlin.jvm.functions.Function0<java.lang.Boolean> defaultValue);
+ method public inline double getDouble(String key);
+ method public inline double getDoubleOrElse(String key, kotlin.jvm.functions.Function0<java.lang.Double> defaultValue);
+ method public inline float getFloat(String key);
+ method public inline float getFloatOrElse(String key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public inline int getInt(String key);
+ method public inline java.util.List<java.lang.Integer> getIntList(String key);
+ method public inline java.util.List<java.lang.Integer> getIntListOrElse(String key, kotlin.jvm.functions.Function0<? extends java.util.List<java.lang.Integer>> defaultValue);
+ method public inline int getIntOrElse(String key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public inline <reified T extends android.os.Parcelable> T getParcelable(String key);
+ method public inline <reified T extends android.os.Parcelable> java.util.List<T> getParcelableList(String key);
+ method public inline <reified T extends android.os.Parcelable> java.util.List<T> getParcelableListOrElse(String key, kotlin.jvm.functions.Function0<? extends java.util.List<? extends T>> defaultValue);
+ method public inline <reified T extends android.os.Parcelable> T getParcelableOrElse(String key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+ method public inline android.os.Bundle getSavedState(String key);
+ method public inline android.os.Bundle getSavedStateOrElse(String key, kotlin.jvm.functions.Function0<android.os.Bundle> defaultValue);
+ method public android.os.Bundle getSource();
+ method public inline String getString(String key);
+ method public inline java.util.List<java.lang.String> getStringList(String key);
+ method public inline java.util.List<java.lang.String> getStringListOrElse(String key, kotlin.jvm.functions.Function0<? extends java.util.List<java.lang.String>> defaultValue);
+ method public inline String getStringOrElse(String key, kotlin.jvm.functions.Function0<java.lang.String> defaultValue);
+ method public inline boolean isEmpty();
+ method public inline int size();
+ property public final android.os.Bundle source;
+ }
+
public final class SavedStateRegistry {
method @MainThread public android.os.Bundle? consumeRestoredStateForKey(String key);
method public androidx.savedstate.SavedStateRegistry.SavedStateProvider? getSavedStateProvider(String key);
@@ -38,6 +76,29 @@
property public abstract androidx.savedstate.SavedStateRegistry savedStateRegistry;
}
+ @kotlin.jvm.JvmInline public final value class SavedStateWriter {
+ ctor public SavedStateWriter(android.os.Bundle source);
+ method public inline void clear();
+ method public android.os.Bundle getSource();
+ method public inline void putAll(android.os.Bundle values);
+ method public inline void putBoolean(String key, boolean value);
+ method public inline void putDouble(String key, double value);
+ method public inline void putFloat(String key, float value);
+ method public inline void putInt(String key, int value);
+ method public inline void putIntList(String key, java.util.List<java.lang.Integer> values);
+ method public inline <reified T extends android.os.Parcelable> void putParcelable(String key, T value);
+ method public inline <reified T extends android.os.Parcelable> void putParcelableList(String key, java.util.List<? extends T> values);
+ method public inline void putSavedState(String key, android.os.Bundle value);
+ method public inline void putString(String key, String value);
+ method public inline void putStringList(String key, java.util.List<java.lang.String> values);
+ method public inline void remove(String key);
+ property public final android.os.Bundle source;
+ }
+
+ public final class SavedState_androidKt {
+ method public static inline android.os.Bundle savedState(optional kotlin.jvm.functions.Function1<? super androidx.savedstate.SavedStateWriter,kotlin.Unit> block);
+ }
+
public final class ViewKt {
method @Deprecated public static androidx.savedstate.SavedStateRegistryOwner? findViewTreeSavedStateRegistryOwner(android.view.View view);
}
diff --git a/savedstate/savedstate/api/restricted_current.txt b/savedstate/savedstate/api/restricted_current.txt
index 1e9d3e6..27b0b57 100644
--- a/savedstate/savedstate/api/restricted_current.txt
+++ b/savedstate/savedstate/api/restricted_current.txt
@@ -1,6 +1,48 @@
// Signature format: 4.0
package androidx.savedstate {
+ public final class SavedStateKt {
+ method public static inline <T> T read(androidx.savedstate.SavedState, kotlin.jvm.functions.Function1<? super androidx.savedstate.SavedStateReader,? extends T> block);
+ method public static inline <T> T read(androidx.savedstate.SavedState, kotlin.jvm.functions.Function1<? super androidx.savedstate.SavedStateReader,? extends T> block);
+ method public static androidx.savedstate.SavedState reader(androidx.savedstate.SavedState);
+ method public static inline <T> T write(androidx.savedstate.SavedState, kotlin.jvm.functions.Function1<? super androidx.savedstate.SavedStateWriter,? extends T> block);
+ method public static inline <T> T write(androidx.savedstate.SavedState, kotlin.jvm.functions.Function1<? super androidx.savedstate.SavedStateWriter,? extends T> block);
+ method public static androidx.savedstate.SavedState writer(androidx.savedstate.SavedState);
+ }
+
+ @kotlin.jvm.JvmInline public final value class SavedStateReader {
+ ctor public SavedStateReader(android.os.Bundle source);
+ method public inline operator boolean contains(String key);
+ method public inline boolean getBoolean(String key);
+ method public inline boolean getBooleanOrElse(String key, kotlin.jvm.functions.Function0<java.lang.Boolean> defaultValue);
+ method public inline double getDouble(String key);
+ method public inline double getDoubleOrElse(String key, kotlin.jvm.functions.Function0<java.lang.Double> defaultValue);
+ method public inline float getFloat(String key);
+ method public inline float getFloatOrElse(String key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+ method public inline int getInt(String key);
+ method public inline java.util.List<java.lang.Integer> getIntList(String key);
+ method public inline java.util.List<java.lang.Integer> getIntListOrElse(String key, kotlin.jvm.functions.Function0<? extends java.util.List<java.lang.Integer>> defaultValue);
+ method public inline int getIntOrElse(String key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method @kotlin.PublishedApi internal inline <reified T> java.util.List<T> getListResultOrElse(String key, kotlin.jvm.functions.Function0<? extends java.util.List<? extends T>> defaultValue, kotlin.jvm.functions.Function0<? extends java.util.List<? extends T>?> currentValue);
+ method @kotlin.PublishedApi internal inline <reified T> java.util.List<T> getListResultOrThrow(String key, kotlin.jvm.functions.Function0<? extends java.util.List<? extends T>?> currentValue);
+ method public inline <reified T extends android.os.Parcelable> T getParcelable(String key);
+ method public inline <reified T extends android.os.Parcelable> java.util.List<T> getParcelableList(String key);
+ method public inline <reified T extends android.os.Parcelable> java.util.List<T> getParcelableListOrElse(String key, kotlin.jvm.functions.Function0<? extends java.util.List<? extends T>> defaultValue);
+ method public inline <reified T extends android.os.Parcelable> T getParcelableOrElse(String key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+ method public inline android.os.Bundle getSavedState(String key);
+ method public inline android.os.Bundle getSavedStateOrElse(String key, kotlin.jvm.functions.Function0<android.os.Bundle> defaultValue);
+ method @kotlin.PublishedApi internal inline <reified T> T getSingleResultOrElse(String key, kotlin.jvm.functions.Function0<? extends T> defaultValue, kotlin.jvm.functions.Function0<? extends T?> currentValue);
+ method @kotlin.PublishedApi internal inline <reified T> T getSingleResultOrThrow(String key, kotlin.jvm.functions.Function0<? extends T?> currentValue);
+ method public android.os.Bundle getSource();
+ method public inline String getString(String key);
+ method public inline java.util.List<java.lang.String> getStringList(String key);
+ method public inline java.util.List<java.lang.String> getStringListOrElse(String key, kotlin.jvm.functions.Function0<? extends java.util.List<java.lang.String>> defaultValue);
+ method public inline String getStringOrElse(String key, kotlin.jvm.functions.Function0<java.lang.String> defaultValue);
+ method public inline boolean isEmpty();
+ method public inline int size();
+ property public final android.os.Bundle source;
+ }
+
public final class SavedStateRegistry {
method @MainThread public android.os.Bundle? consumeRestoredStateForKey(String key);
method public androidx.savedstate.SavedStateRegistry.SavedStateProvider? getSavedStateProvider(String key);
@@ -38,6 +80,30 @@
property public abstract androidx.savedstate.SavedStateRegistry savedStateRegistry;
}
+ @kotlin.jvm.JvmInline public final value class SavedStateWriter {
+ ctor public SavedStateWriter(android.os.Bundle source);
+ method public inline void clear();
+ method public android.os.Bundle getSource();
+ method public inline void putAll(android.os.Bundle values);
+ method public inline void putBoolean(String key, boolean value);
+ method public inline void putDouble(String key, double value);
+ method public inline void putFloat(String key, float value);
+ method public inline void putInt(String key, int value);
+ method public inline void putIntList(String key, java.util.List<java.lang.Integer> values);
+ method public inline <reified T extends android.os.Parcelable> void putParcelable(String key, T value);
+ method public inline <reified T extends android.os.Parcelable> void putParcelableList(String key, java.util.List<? extends T> values);
+ method public inline void putSavedState(String key, android.os.Bundle value);
+ method public inline void putString(String key, String value);
+ method public inline void putStringList(String key, java.util.List<java.lang.String> values);
+ method public inline void remove(String key);
+ method @kotlin.PublishedApi internal inline <reified T> java.util.ArrayList<T> toArrayListUnsafe(java.util.Collection<? extends java.lang.Object?>);
+ property public final android.os.Bundle source;
+ }
+
+ public final class SavedState_androidKt {
+ method public static inline android.os.Bundle savedState(optional kotlin.jvm.functions.Function1<? super androidx.savedstate.SavedStateWriter,kotlin.Unit> block);
+ }
+
public final class ViewKt {
method @Deprecated public static androidx.savedstate.SavedStateRegistryOwner? findViewTreeSavedStateRegistryOwner(android.view.View view);
}
@@ -49,3 +115,16 @@
}
+package androidx.savedstate.internal {
+
+ @kotlin.PublishedApi internal final class SavedStateUtils {
+ method public inline <reified T> T getValueFromSavedState(String key, kotlin.jvm.functions.Function0<? extends T?> currentValue, kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> contains, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+ method public inline Void keyNotFoundError(String key);
+ field public static final boolean DEFAULT_BOOLEAN = false;
+ field public static final double DEFAULT_DOUBLE = 0.0;
+ field public static final float DEFAULT_FLOAT = 0.0f;
+ field public static final int DEFAULT_INT = 0; // 0x0
+ }
+
+}
+
diff --git a/savedstate/savedstate/build.gradle b/savedstate/savedstate/build.gradle
index b863381..86350a0 100644
--- a/savedstate/savedstate/build.gradle
+++ b/savedstate/savedstate/build.gradle
@@ -5,10 +5,9 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.PlatformIdentifier
+
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
-import org.jetbrains.kotlin.konan.target.Family
+import androidx.build.PlatformIdentifier
plugins {
id("AndroidXPlugin")
@@ -23,11 +22,21 @@
sourceSets {
commonMain {
- // TODO(b/334076622)
+ dependencies {
+ api(libs.kotlinStdlib)
+ api("androidx.annotation:annotation:1.8.0")
+ api(projectOrArtifact(":lifecycle:lifecycle-common"))
+ }
}
commonTest {
- // TODO(b/334076622)
+ dependencies {
+ implementation(project(":kruth:kruth"))
+ implementation(libs.kotlinTest)
+ implementation(libs.kotlinTestCommon)
+ implementation(libs.kotlinTestAnnotationsCommon)
+ implementation(libs.kotlinCoroutinesTest)
+ }
}
jvmMain {
@@ -42,12 +51,20 @@
dependsOn(jvmMain)
dependencies {
api("androidx.annotation:annotation:1.8.1")
+ implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.arch.core:core-common:2.2.0")
implementation("androidx.lifecycle:lifecycle-common:2.6.1")
api(libs.kotlinStdlib)
}
}
+ androidUnitTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.robolectric)
+ }
+ }
+
androidInstrumentedTest {
dependsOn(jvmTest)
dependencies {
@@ -60,8 +77,22 @@
}
}
+ nonAndroidMain {
+ dependsOn(commonMain)
+ }
+
+ nonAndroidTest {
+ dependsOn(commonTest)
+ }
+
desktopMain {
dependsOn(jvmMain)
+ dependsOn(nonAndroidMain)
+ }
+
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(nonAndroidTest)
}
}
}
diff --git a/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/RobolectricTest.android.kt b/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/RobolectricTest.android.kt
new file mode 100644
index 0000000..d72a1d9
--- /dev/null
+++ b/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/RobolectricTest.android.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.savedstate
+
+internal actual abstract class RobolectricTest actual constructor()
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/Recreator.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/Recreator.android.kt
index 82bc439..643b555 100644
--- a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/Recreator.android.kt
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/Recreator.android.kt
@@ -15,7 +15,6 @@
*/
package androidx.savedstate
-import android.os.Bundle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
@@ -28,15 +27,19 @@
throw AssertionError("Next event must be ON_CREATE")
}
source.lifecycle.removeObserver(this)
- val bundle: Bundle =
- owner.savedStateRegistry.consumeRestoredStateForKey(COMPONENT_KEY) ?: return
- val classes: MutableList<String> =
- bundle.getStringArrayList(CLASSES_KEY)
- ?: throw IllegalStateException(
- "Bundle with restored state for the component " +
- "\"$COMPONENT_KEY\" must contain list of strings by the key " +
- "\"$CLASSES_KEY\""
- )
+
+ val registry = owner.savedStateRegistry
+ val savedState = registry.consumeRestoredStateForKey(COMPONENT_KEY) ?: return
+ val classes =
+ savedState.read {
+ return@read getStringListOrElse(CLASSES_KEY) {
+ error(
+ "SavedState with restored state for the component " +
+ "\"$COMPONENT_KEY\" must contain list of strings by the key " +
+ "\"$CLASSES_KEY\""
+ )
+ }
+ }
for (className: String in classes) {
reflectiveNew(className)
}
@@ -79,8 +82,8 @@
registry.registerSavedStateProvider(COMPONENT_KEY, this)
}
- override fun saveState(): Bundle {
- return Bundle().apply { putStringArrayList(CLASSES_KEY, ArrayList(classes)) }
+ override fun saveState(): SavedState = savedState {
+ putStringList(CLASSES_KEY, classes.toList())
}
fun add(className: String) {
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedState.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedState.android.kt
new file mode 100644
index 0000000..6d51126
--- /dev/null
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedState.android.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.savedstate
+
+public actual typealias SavedState = android.os.Bundle
+
+public actual inline fun savedState(block: SavedStateWriter.() -> Unit): SavedState =
+ SavedState().apply { write(block) }
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateReader.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateReader.android.kt
new file mode 100644
index 0000000..73eea19
--- /dev/null
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateReader.android.kt
@@ -0,0 +1,215 @@
+/*
+ * 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.savedstate
+
+import android.os.Parcelable
+import androidx.core.os.BundleCompat
+import androidx.savedstate.internal.SavedStateUtils
+import androidx.savedstate.internal.SavedStateUtils.getValueFromSavedState
+import androidx.savedstate.internal.SavedStateUtils.keyNotFoundError
+
+@Suppress("NOTHING_TO_INLINE")
+@JvmInline
+actual value class SavedStateReader actual constructor(actual val source: SavedState) {
+
+ actual inline fun getBoolean(key: String): Boolean {
+ return getSingleResultOrThrow(key) {
+ source.getBoolean(key, SavedStateUtils.DEFAULT_BOOLEAN)
+ }
+ }
+
+ actual inline fun getBooleanOrElse(key: String, defaultValue: () -> Boolean): Boolean {
+ return getSingleResultOrElse(key, defaultValue) { source.getBoolean(key) }
+ }
+
+ actual inline fun getDouble(key: String): Double {
+ return getSingleResultOrThrow(key) { source.getDouble(key, SavedStateUtils.DEFAULT_DOUBLE) }
+ }
+
+ actual inline fun getDoubleOrElse(key: String, defaultValue: () -> Double): Double {
+ return getSingleResultOrElse(key, defaultValue) { source.getDouble(key) }
+ }
+
+ actual inline fun getFloat(key: String): Float {
+ return getSingleResultOrThrow(key) { source.getFloat(key, SavedStateUtils.DEFAULT_FLOAT) }
+ }
+
+ actual inline fun getFloatOrElse(key: String, defaultValue: () -> Float): Float {
+ return getSingleResultOrElse(key, defaultValue) { source.getFloat(key) }
+ }
+
+ actual inline fun getInt(key: String): Int {
+ return getSingleResultOrThrow(key) { source.getInt(key, SavedStateUtils.DEFAULT_INT) }
+ }
+
+ actual inline fun getIntOrElse(key: String, defaultValue: () -> Int): Int {
+ return getSingleResultOrElse(key, defaultValue) { source.getInt(key) }
+ }
+
+ /**
+ * Retrieves a [Parcelable] object associated with the specified key. Throws an
+ * [IllegalStateException] if the key doesn't exist.
+ *
+ * @param key The key to retrieve the value for.
+ * @return The [Parcelable] object associated with the key.
+ * @throws IllegalStateException If the key is not found.
+ */
+ inline fun <reified T : Parcelable> getParcelable(key: String): T {
+ return getSingleResultOrThrow(key) {
+ BundleCompat.getParcelable(source, key, T::class.java)
+ }
+ }
+
+ /**
+ * Retrieves a [Parcelable] object associated with the specified key, or a default value if the
+ * key doesn't exist.
+ *
+ * @param key The key to retrieve the value for.
+ * @param defaultValue A function providing the default [Parcelable] if the key is not found.
+ * @return The [Parcelable] object associated with the key, or the default value if the key is
+ * not found.
+ */
+ inline fun <reified T : Parcelable> getParcelableOrElse(key: String, defaultValue: () -> T): T {
+ return getSingleResultOrElse(key, defaultValue) {
+ BundleCompat.getParcelable(source, key, T::class.java)
+ }
+ }
+
+ actual inline fun getString(key: String): String {
+ return getSingleResultOrThrow(key) { source.getString(key) }
+ }
+
+ actual inline fun getStringOrElse(key: String, defaultValue: () -> String): String {
+ return getSingleResultOrElse(key, defaultValue) { source.getString(key) }
+ }
+
+ actual inline fun getIntList(key: String): List<Int> {
+ return getListResultOrThrow(key) { source.getIntegerArrayList(key) }
+ }
+
+ actual inline fun getIntListOrElse(key: String, defaultValue: () -> List<Int>): List<Int> {
+ return getListResultOrElse(key, defaultValue) { source.getIntegerArrayList(key) }
+ }
+
+ actual inline fun getStringList(key: String): List<String> {
+ return getListResultOrThrow(key) { source.getStringArrayList(key) }
+ }
+
+ actual inline fun getStringListOrElse(
+ key: String,
+ defaultValue: () -> List<String>
+ ): List<String> {
+ return getListResultOrElse(key, defaultValue) { source.getStringArrayList(key) }
+ }
+
+ /**
+ * Retrieves a [List] of elements of [Parcelable] associated with the specified [key]. Throws an
+ * [IllegalStateException] if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @return The [List] of elements of [Parcelable] associated with the [key].
+ * @throws IllegalStateException If the [key] is not found.
+ */
+ inline fun <reified T : Parcelable> getParcelableList(key: String): List<T> {
+ return getListResultOrThrow(key) {
+ BundleCompat.getParcelableArrayList(source, key, T::class.java)
+ }
+ }
+
+ /**
+ * Retrieves a [List] of elements of [Parcelable] associated with the specified [key], or a
+ * default value if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @param defaultValue A function providing the default value if the [key] is not found or the
+ * retrieved value is not a list of [Parcelable].
+ * @return The list of elements of [Parcelable] associated with the [key], or the default value
+ * if the [key] is not found.
+ */
+ inline fun <reified T : Parcelable> getParcelableListOrElse(
+ key: String,
+ defaultValue: () -> List<T>
+ ): List<T> {
+ return getListResultOrElse(key, defaultValue) {
+ BundleCompat.getParcelableArrayList(source, key, T::class.java)
+ }
+ }
+
+ actual inline fun getSavedState(key: String): SavedState {
+ return getSingleResultOrThrow(key) { source.getBundle(key) }
+ }
+
+ actual inline fun getSavedStateOrElse(key: String, defaultValue: () -> SavedState): SavedState {
+ return getSingleResultOrElse(key, defaultValue) { source.getBundle(key) }
+ }
+
+ actual inline fun size(): Int = source.size()
+
+ actual inline fun isEmpty(): Boolean = source.isEmpty
+
+ actual inline operator fun contains(key: String): Boolean = source.containsKey(key)
+
+ @PublishedApi
+ internal inline fun <reified T> getSingleResultOrThrow(
+ key: String,
+ currentValue: () -> T?,
+ ): T =
+ getValueFromSavedState(
+ key = key,
+ contains = { source.containsKey(key) },
+ currentValue = { currentValue() },
+ defaultValue = { keyNotFoundError(key) },
+ )
+
+ @PublishedApi
+ internal inline fun <reified T> getSingleResultOrElse(
+ key: String,
+ defaultValue: () -> T,
+ currentValue: () -> T?,
+ ): T =
+ getValueFromSavedState(
+ key = key,
+ contains = { source.containsKey(key) },
+ currentValue = { currentValue() },
+ defaultValue = { defaultValue() },
+ )
+
+ @PublishedApi
+ internal inline fun <reified T> getListResultOrThrow(
+ key: String,
+ currentValue: () -> List<T>?,
+ ): List<T> =
+ getValueFromSavedState(
+ key = key,
+ contains = { source.containsKey(key) },
+ currentValue = { currentValue() },
+ defaultValue = { keyNotFoundError(key) },
+ )
+
+ @PublishedApi
+ internal inline fun <reified T> getListResultOrElse(
+ key: String,
+ defaultValue: () -> List<T>,
+ currentValue: () -> List<T>?,
+ ): List<T> =
+ getValueFromSavedState(
+ key = key,
+ contains = { source.containsKey(key) },
+ currentValue = { currentValue() },
+ defaultValue = { defaultValue() },
+ )
+}
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistry.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistry.android.kt
index d8427e8..0a7bb5b 100644
--- a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistry.android.kt
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistry.android.kt
@@ -15,7 +15,6 @@
*/
package androidx.savedstate
-import android.os.Bundle
import androidx.annotation.MainThread
import androidx.arch.core.internal.SafeIterableMap
import androidx.lifecycle.Lifecycle
@@ -30,7 +29,7 @@
class SavedStateRegistry internal constructor() {
private val components = SafeIterableMap<String, SavedStateProvider>()
private var attached = false
- private var restoredState: Bundle? = null
+ private var restoredState: SavedState? = null
/**
* Whether the state was restored after creation and can be safely consumed with
@@ -52,7 +51,7 @@
* This call clears an internal reference to returned saved state, so if you call it second time
* in the row it will return `null`.
*
- * All unconsumed values will be saved during `onSaveInstanceState(Bundle savedState)`
+ * All unconsumed values will be saved during `onSaveInstanceState(SavedState savedState)`
*
* This method can be called after `super.onCreate(savedStateBundle)` of the corresponding
* component. Calling it before that will result in `IllegalArgumentException`.
@@ -63,20 +62,21 @@
* @return `S` with the previously saved state or {@code null}
*/
@MainThread
- fun consumeRestoredStateForKey(key: String): Bundle? {
+ fun consumeRestoredStateForKey(key: String): SavedState? {
check(isRestored) {
- ("You can consumeRestoredStateForKey " +
- "only after super.onCreate of corresponding component")
+ "You can consumeRestoredStateForKey " +
+ "only after super.onCreate of corresponding component"
}
- if (restoredState != null) {
- val result = restoredState?.getBundle(key)
- restoredState?.remove(key)
- if (restoredState?.isEmpty != false) {
- restoredState = null
- }
- return result
+
+ val state = restoredState ?: return null
+
+ val consumed = state.read { if (contains(key)) getSavedState(key) else null }
+ state.write { remove(key) }
+ if (state.read { isEmpty() }) {
+ restoredState = null
}
- return null
+
+ return consumed
}
/**
@@ -96,9 +96,7 @@
@MainThread
fun registerSavedStateProvider(key: String, provider: SavedStateProvider) {
val previous = components.putIfAbsent(key, provider)
- require(previous == null) {
- ("SavedStateProvider with the given key is" + " already registered")
- }
+ require(previous == null) { "SavedStateProvider with the given key is already registered" }
}
/**
@@ -193,13 +191,16 @@
/** An interface for an owner of this [SavedStateRegistry] to restore saved state. */
@MainThread
- internal fun performRestore(savedState: Bundle?) {
+ internal fun performRestore(savedState: SavedState?) {
check(attached) {
- ("You must call performAttach() before calling " + "performRestore(Bundle).")
+ "You must call performAttach() before calling performRestore(SavedState)."
}
check(!isRestored) { "SavedStateRegistry was already restored." }
- restoredState = savedState?.getBundle(SAVED_COMPONENTS_KEY)
+ restoredState =
+ savedState?.read {
+ if (contains(SAVED_COMPONENTS_KEY)) getSavedState(SAVED_COMPONENTS_KEY) else null
+ }
isRestored = true
}
@@ -207,22 +208,19 @@
* An interface for an owner of this [SavedStateRegistry] to perform state saving, it will call
* all registered providers and merge with unconsumed state.
*
- * @param outBundle Bundle in which to place a saved state
+ * @param outBundle SavedState in which to place a saved state
*/
@MainThread
- internal fun performSave(outBundle: Bundle) {
- val components = Bundle()
- if (restoredState != null) {
- components.putAll(restoredState)
+ internal fun performSave(outBundle: SavedState) {
+ val inState = savedState {
+ restoredState?.let { putAll(it) }
+ for ((key, provider) in components) {
+ putSavedState(key, provider.saveState())
+ }
}
- val it: Iterator<Map.Entry<String, SavedStateProvider>> =
- this.components.iteratorWithAdditions()
- while (it.hasNext()) {
- val (key, value) = it.next()
- components.putBundle(key, value.saveState())
- }
- if (!components.isEmpty) {
- outBundle.putBundle(SAVED_COMPONENTS_KEY, components)
+
+ if (inState.read { !isEmpty() }) {
+ outBundle.write { putSavedState(SAVED_COMPONENTS_KEY, inState) }
}
}
@@ -234,7 +232,7 @@
*
* Returns `S` with your saved state.
*/
- fun saveState(): Bundle
+ fun saveState(): SavedState
}
private companion object {
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistryController.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistryController.android.kt
index 5b5fd0b..6956e6e 100644
--- a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistryController.android.kt
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistryController.android.kt
@@ -15,7 +15,6 @@
*/
package androidx.savedstate
-import android.os.Bundle
import androidx.annotation.MainThread
import androidx.lifecycle.Lifecycle
@@ -54,7 +53,7 @@
* @param savedState restored state
*/
@MainThread
- fun performRestore(savedState: Bundle?) {
+ fun performRestore(savedState: SavedState?) {
// To support backward compatibility with libraries that do not explicitly
// call performAttach(), we make sure that work is done here
if (!attached) {
@@ -71,10 +70,10 @@
* An interface for an owner of this [SavedStateRegistry] to perform state saving, it will call
* all registered providers and merge with unconsumed state.
*
- * @param outBundle Bundle in which to place a saved state
+ * @param outBundle SavedState in which to place a saved state
*/
@MainThread
- fun performSave(outBundle: Bundle) {
+ fun performSave(outBundle: SavedState) {
savedStateRegistry.performSave(outBundle)
}
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistryOwner.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistryOwner.android.kt
index a938673..dee87c7 100644
--- a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistryOwner.android.kt
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateRegistryOwner.android.kt
@@ -30,7 +30,7 @@
* call [SavedStateRegistryController.performRestore]
*
* [SavedStateRegistryController.performRestore] can be called with a nullable if nothing needs to
- * be restored, or with the state Bundle to be restored. performRestore can be called in one of two
+ * be restored, or with the SavedState to be restored. performRestore can be called in one of two
* places:
* 1. Directly before the Lifecycle moves to [Lifecycle.State.CREATED]
* 2. Before [Lifecycle.State.STARTED] is reached, as part of the [LifecycleObserver] that is added
@@ -38,8 +38,8 @@
*
* [SavedStateRegistryController.performSave] should be called after owner has been stopped but
* before it reaches [Lifecycle.State.DESTROYED] state. Hence it should only be called once the
- * owner has received the [Lifecycle.Event.ON_STOP] event. The bundle passed to performSave will be
- * the bundle restored by performRestore.
+ * owner has received the [Lifecycle.Event.ON_STOP] event. The SavedState passed to performSave will
+ * be the SavedState restored by performRestore.
*
* @see [ViewTreeSavedStateRegistryOwner]
*/
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateWriter.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateWriter.android.kt
new file mode 100644
index 0000000..2d1c86e
--- /dev/null
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/SavedStateWriter.android.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.savedstate
+
+import android.os.Parcelable
+
+@Suppress("NOTHING_TO_INLINE")
+@JvmInline
+actual value class SavedStateWriter actual constructor(actual val source: SavedState) {
+
+ actual inline fun putBoolean(key: String, value: Boolean) {
+ source.putBoolean(key, value)
+ }
+
+ actual inline fun putDouble(key: String, value: Double) {
+ source.putDouble(key, value)
+ }
+
+ actual inline fun putFloat(key: String, value: Float) {
+ source.putFloat(key, value)
+ }
+
+ actual inline fun putInt(key: String, value: Int) {
+ source.putInt(key, value)
+ }
+
+ /**
+ * Stores an [Parcelable] value associated with the specified key in the [SavedState].
+ *
+ * @param key The key to associate the value with.
+ * @param value The [Parcelable] value to store.
+ */
+ inline fun <reified T : Parcelable> putParcelable(key: String, value: T) {
+ source.putParcelable(key, value)
+ }
+
+ actual inline fun putString(key: String, value: String) {
+ source.putString(key, value)
+ }
+
+ actual inline fun putIntList(key: String, values: List<Int>) {
+ source.putIntegerArrayList(key, values.toArrayListUnsafe())
+ }
+
+ actual inline fun putStringList(key: String, values: List<String>) {
+ source.putStringArrayList(key, values.toArrayListUnsafe())
+ }
+
+ /**
+ * Stores a list of elements of [Parcelable] associated with the specified key in the
+ * [SavedState].
+ *
+ * @param key The key to associate the value with.
+ * @param values The list of elements to store.
+ */
+ inline fun <reified T : Parcelable> putParcelableList(key: String, values: List<T>) {
+ source.putParcelableArrayList(key, values.toArrayListUnsafe())
+ }
+
+ actual inline fun putSavedState(key: String, value: SavedState) {
+ source.putBundle(key, value)
+ }
+
+ actual inline fun putAll(values: SavedState) {
+ source.putAll(values)
+ }
+
+ actual inline fun remove(key: String) {
+ source.remove(key)
+ }
+
+ actual inline fun clear() {
+ source.clear()
+ }
+
+ @Suppress("UNCHECKED_CAST", "ConcreteCollection")
+ @PublishedApi
+ internal inline fun <reified T : Any> Collection<*>.toArrayListUnsafe(): ArrayList<T> {
+ return if (this is ArrayList<*>) this as ArrayList<T> else ArrayList(this as Collection<T>)
+ }
+}
diff --git a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/ParcelableSavedStateTest.android.kt b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/ParcelableSavedStateTest.android.kt
new file mode 100644
index 0000000..48f76bf
--- /dev/null
+++ b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/ParcelableSavedStateTest.android.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.savedstate
+
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.kruth.assertThat
+import androidx.kruth.assertThrows
+import kotlin.test.Test
+
+internal class ParcelableSavedStateTest : RobolectricTest() {
+
+ @Test
+ fun getParcelable_whenSet_returns() {
+ val underTest = savedState { putParcelable(KEY_1, PARCELABLE_VALUE_1) }
+ val actual = underTest.read { getParcelable<TestParcelable>(KEY_1) }
+
+ assertThat(actual).isEqualTo(PARCELABLE_VALUE_1)
+ }
+
+ @Test
+ fun getParcelable_whenNotSet_throws() {
+ assertThrows<IllegalStateException> {
+ savedState().read { getParcelable<TestParcelable>(KEY_1) }
+ }
+ }
+
+ @Test
+ fun getParcelable_whenSet_differentType_returnsDefault() {
+ val underTest = savedState { putInt(KEY_1, Int.MAX_VALUE) }
+
+ assertThrows<IllegalStateException> {
+ underTest.read { getParcelable<TestParcelable>(KEY_1) }
+ }
+ }
+
+ @Test
+ fun getParcelableOrElse_whenSet_returns() {
+ val underTest = savedState { putParcelable(KEY_1, PARCELABLE_VALUE_1) }
+ val actual = underTest.read { getParcelableOrElse(KEY_1) { PARCELABLE_VALUE_2 } }
+
+ assertThat(actual).isEqualTo(PARCELABLE_VALUE_1)
+ }
+
+ @Test
+ fun getParcelableOrElse_whenNotSet_returnsElse() {
+ val actual = savedState().read { getParcelableOrElse(KEY_1) { PARCELABLE_VALUE_1 } }
+
+ assertThat(actual).isEqualTo(PARCELABLE_VALUE_1)
+ }
+
+ @Test
+ fun getParcelableList_whenSet_returns() {
+ val expected = List(size = 5) { idx -> TestParcelable(idx) }
+
+ val underTest = savedState { putParcelableList(KEY_1, expected) }
+ val actual = underTest.read { getParcelableList<TestParcelable>(KEY_1) }
+
+ assertThat(actual).isEqualTo(expected)
+ }
+
+ @Test
+ fun getList_ofParcelable_whenNotSet_throws() {
+ assertThrows<IllegalStateException> {
+ savedState().read { getParcelableList<TestParcelable>(KEY_1) }
+ }
+ }
+
+ @Test
+ fun getListofParcelable_whenSet_differentType_throws() {
+ val underTest = savedState { putInt(KEY_1, Int.MAX_VALUE) }
+
+ assertThrows<IllegalStateException> {
+ underTest.read { getParcelableList<TestParcelable>(KEY_1) }
+ }
+ }
+
+ @Test
+ fun getListOrElse_ofParcelable_whenSet_returns() {
+ val expected = List(size = 5) { idx -> TestParcelable(idx) }
+
+ val underTest = savedState { putParcelableList(KEY_1, expected) }
+ val actual =
+ underTest.read { getParcelableListOrElse<TestParcelable>(KEY_1) { emptyList() } }
+
+ assertThat(actual).isEqualTo(expected)
+ }
+
+ @Test
+ fun getListOrElse_ofParcelable_whenNotSet_returnsElse() {
+ val actual =
+ savedState().read { getParcelableListOrElse<TestParcelable>(KEY_1) { emptyList() } }
+
+ assertThat(actual).isEqualTo(emptyList<TestParcelable>())
+ }
+
+ private companion object {
+ const val KEY_1 = "KEY_1"
+ val PARCELABLE_VALUE_1 = TestParcelable(value = Int.MIN_VALUE)
+ val PARCELABLE_VALUE_2 = TestParcelable(value = Int.MAX_VALUE)
+ }
+
+ internal data class TestParcelable(val value: Int) : Parcelable {
+
+ override fun describeContents(): Int = 0
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ dest.writeInt(value)
+ }
+
+ companion object {
+ @Suppress("unused")
+ @JvmField
+ val CREATOR =
+ object : Parcelable.Creator<TestParcelable> {
+ override fun createFromParcel(source: Parcel) =
+ TestParcelable(value = source.readInt())
+
+ override fun newArray(size: Int) = arrayOfNulls<TestParcelable>(size)
+ }
+ }
+ }
+}
diff --git a/camera/camera-media3-effect/src/test/java/androidx/camera/media3/effect/Media3EffectTest.kt b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/RobolectricTest.android.kt
similarity index 63%
rename from camera/camera-media3-effect/src/test/java/androidx/camera/media3/effect/Media3EffectTest.kt
rename to savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/RobolectricTest.android.kt
index 0f3365e..836b692 100644
--- a/camera/camera-media3-effect/src/test/java/androidx/camera/media3/effect/Media3EffectTest.kt
+++ b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/RobolectricTest.android.kt
@@ -14,25 +14,12 @@
* limitations under the License.
*/
-package androidx.camera.media3.effect
+package androidx.savedstate
-import android.os.Build
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-/** Unit tests for [Media3Effect]. */
@RunWith(RobolectricTestRunner::class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-class Media3EffectTest {
-
- // TODO: replace this with a real test.
- @Test
- fun smokeTest() {
- assertThat(true).isTrue()
- }
-}
+@Config(manifest = Config.NONE)
+internal actual abstract class RobolectricTest actual constructor()
diff --git a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedState.kt b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedState.kt
new file mode 100644
index 0000000..710bdd0
--- /dev/null
+++ b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedState.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.savedstate
+
+/**
+ * An opaque (empty) common type that holds saveable values to be saved and restored by native
+ * platforms that have a concept of System-initiated Process Death.
+ *
+ * That means, the OS will give the chance for the process to keep the state of the application
+ * (normally using a serialization mechanism), and allow the app to restore its state later. That is
+ * commonly referred to as "state restoration".
+ *
+ * required to act as a source input for a [SavedStateReader] or [SavedStateWriter].
+ *
+ * This class represents a container for persistable state data. It is designed to be
+ * platform-agnostic, allowing seamless state saving and restoration across different environments.
+ */
+public expect class SavedState
+
+/** Constructs an empty [SavedState] instance. */
+public expect inline fun savedState(block: SavedStateWriter.() -> Unit = {}): SavedState
+
+/** Creates a new [SavedStateReader] for the [SavedState]. */
+public fun SavedState.reader(): SavedStateReader = SavedStateReader(source = this)
+
+/** Creates a new [SavedStateWriter] for the [SavedState]. */
+public fun SavedState.writer(): SavedStateWriter = SavedStateWriter(source = this)
+
+/**
+ * Calls the specified function [block] with a [SavedStateReader] value as its receiver and returns
+ * the [block] value.
+ *
+ * @param block A lambda function that performs read operations using the [SavedStateReader].
+ * @return The result of the lambda function's execution.
+ * @see [SavedStateReader]
+ * @see [SavedStateWriter]
+ */
+public inline fun <T> SavedState.read(block: SavedStateReader.() -> T): T {
+ return block(reader())
+}
+
+/**
+ * Calls the specified function [block] with a [SavedStateReader] value as its receiver and returns
+ * the [block] value.
+ *
+ * @param block A lambda function that performs read operations using the [SavedStateReader].
+ * @return The result of the lambda function's execution.
+ * @see [SavedStateReader]
+ * @see [SavedStateWriter]
+ */
+public inline fun <T> SavedStateWriter.read(block: SavedStateReader.() -> T): T {
+ return source.read(block)
+}
+
+/**
+ * Calls the specified function [block] with a [SavedStateWriter] value as its receiver and returns
+ * the [block] value.
+ *
+ * @param block A lambda function that performs write operations using the [SavedStateWriter].
+ * @return The result of the lambda function's execution.
+ * @see [SavedStateReader]
+ * @see [SavedStateWriter]
+ */
+public inline fun <T> SavedState.write(block: SavedStateWriter.() -> T): T {
+ return block(writer())
+}
+
+/**
+ * Calls the specified function [block] with a [SavedStateWriter] value as its receiver and returns
+ * the [block] value.
+ *
+ * @param block A lambda function that performs write operations using the [SavedStateWriter].
+ * @return The result of the lambda function's execution.
+ * @see [SavedStateReader]
+ * @see [SavedStateWriter]
+ */
+public inline fun <T> SavedStateReader.write(block: SavedStateWriter.() -> T): T {
+ return source.write(block)
+}
diff --git a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedStateReader.kt b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedStateReader.kt
new file mode 100644
index 0000000..b0c4477
--- /dev/null
+++ b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedStateReader.kt
@@ -0,0 +1,225 @@
+/*
+ * 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.savedstate
+
+/**
+ * An inline class that encapsulates an opaque [SavedState], and provides an API for reading the
+ * platform specific state.
+ *
+ * @see SavedState.read
+ */
+@JvmInline
+public expect value class SavedStateReader
+internal constructor(
+ @PublishedApi internal val source: SavedState,
+) {
+
+ /**
+ * Retrieves a [Boolean] value associated with the specified [key]. Throws an
+ * [IllegalStateException] if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @return The [Boolean] value associated with the [key].
+ * @throws IllegalStateException If the [key] is not found.
+ */
+ public inline fun getBoolean(key: String): Boolean
+
+ /**
+ * Retrieves a [Boolean] value associated with the specified [key], or a default value if the
+ * [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @param defaultValue A function providing the default value if the [key] is not found.
+ * @return The [Boolean] value associated with the [key], or the default value if the [key] is
+ * not found.
+ */
+ public inline fun getBooleanOrElse(key: String, defaultValue: () -> Boolean): Boolean
+
+ /**
+ * Retrieves a [Double] value associated with the specified [key]. Throws an
+ * [IllegalStateException] if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @return The [Double] value associated with the [key].
+ * @throws IllegalStateException If the [key] is not found.
+ */
+ public inline fun getDouble(key: String): Double
+
+ /**
+ * Retrieves a [Double] value associated with the specified [key], or a default value if the
+ * [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @param defaultValue A function providing the default value if the [key] is not found.
+ * @return The [Double] value associated with the [key], or the default value if the [key] is
+ * not found.
+ */
+ public inline fun getDoubleOrElse(key: String, defaultValue: () -> Double): Double
+
+ /**
+ * Retrieves a [Float] value associated with the specified [key]. Throws an
+ * [IllegalStateException] if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @return The [Float] value associated with the [key].
+ * @throws IllegalStateException If the [key] is not found.
+ */
+ public inline fun getFloat(key: String): Float
+
+ /**
+ * Retrieves a [Float] value associated with the specified [key], or a default value if the
+ * [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @param defaultValue A function providing the default value if the [key] is not found.
+ * @return The [Float] value associated with the [key], or the default value if the [key] is not
+ * found.
+ */
+ public inline fun getFloatOrElse(key: String, defaultValue: () -> Float): Float
+
+ /**
+ * Retrieves an [Int] value associated with the specified [key]. Throws an
+ * [IllegalStateException] if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @return The [Int] value associated with the [key].
+ * @throws IllegalStateException If the [key] is not found.
+ */
+ public inline fun getInt(key: String): Int
+
+ /**
+ * Retrieves an [Int] value associated with the specified [key], or a default value if the [key]
+ * doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @param defaultValue A function providing the default value if the [key] is not found.
+ * @return The [Int] value associated with the [key], or the default value if the [key] is not
+ * found.
+ */
+ public inline fun getIntOrElse(key: String, defaultValue: () -> Int): Int
+
+ /**
+ * Retrieves a [String] value associated with the specified [key]. Throws an
+ * [IllegalStateException] if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @return The [String] value associated with the [key].
+ * @throws IllegalStateException If the [key] is not found.
+ */
+ public inline fun getString(key: String): String
+
+ /**
+ * Retrieves a [String] value associated with the specified [key], or a default value if the
+ * [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @param defaultValue A function providing the default value if the [key] is not found.
+ * @return The [String] value associated with the [key], or the default value if the [key] is
+ * not found.
+ */
+ public inline fun getStringOrElse(key: String, defaultValue: () -> String): String
+
+ /**
+ * Retrieves a [List] of elements of [Int] associated with the specified [key]. Throws an
+ * [IllegalStateException] if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @return The [List] of elements of [Int] associated with the [key].
+ * @throws IllegalStateException If the [key] is not found.
+ */
+ public inline fun getIntList(key: String): List<Int>
+
+ /**
+ * Retrieves a [List] of elements of [Int] associated with the specified [key], or a default
+ * value if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @param defaultValue A function providing the default value if the [key] is not found or the
+ * retrieved value is not a list of [Int].
+ * @return The list of elements of [Int] associated with the [key], or the default value if the
+ * [key] is not found.
+ */
+ public inline fun getIntListOrElse(key: String, defaultValue: () -> List<Int>): List<Int>
+
+ /**
+ * Retrieves a [List] of elements of [String] associated with the specified [key]. Throws an
+ * [IllegalStateException] if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @return The [List] of elements of [String] associated with the [key].
+ * @throws IllegalStateException If the [key] is not found.
+ */
+ public inline fun getStringList(key: String): List<String>
+
+ /**
+ * Retrieves a [List] of elements of [String] associated with the specified [key], or a default
+ * value if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @param defaultValue A function providing the default value if the [key] is not found or the
+ * retrieved value is not a list of [String].
+ * @return The list of elements of [String] associated with the [key], or the default value if
+ * the [key] is not found.
+ */
+ public inline fun getStringListOrElse(
+ key: String,
+ defaultValue: () -> List<String>
+ ): List<String>
+
+ /**
+ * Retrieves a [SavedState] object associated with the specified [key]. Throws an
+ * [IllegalStateException] if the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @return The [SavedState] object associated with the [key].
+ * @throws IllegalStateException If the [key] is not found.
+ */
+ public inline fun getSavedState(key: String): SavedState
+
+ /**
+ * Retrieves a [SavedState] object associated with the specified [key], or a default value if
+ * the [key] doesn't exist.
+ *
+ * @param key The [key] to retrieve the value for.
+ * @param defaultValue A function providing the default [SavedState] if the [key] is not found.
+ * @return The [SavedState] object associated with the [key], or the default value if the [key]
+ * is not found.
+ */
+ public inline fun getSavedStateOrElse(key: String, defaultValue: () -> SavedState): SavedState
+
+ /**
+ * Returns the number of key-value pairs in the [SavedState].
+ *
+ * @return The size of the [SavedState].
+ */
+ public inline fun size(): Int
+
+ /**
+ * Checks if the [SavedState] is empty (contains no key-value pairs).
+ *
+ * @return `true` if the [SavedState] is empty, `false` otherwise.
+ */
+ public inline fun isEmpty(): Boolean
+
+ /**
+ * Checks if the [SavedState] contains the specified [key].
+ *
+ * @param key The [key] to check for.
+ * @return `true` if the [SavedState] contains the [key], `false` otherwise.
+ */
+ public inline operator fun contains(key: String): Boolean
+}
diff --git a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedStateWriter.kt b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedStateWriter.kt
new file mode 100644
index 0000000..67c312c
--- /dev/null
+++ b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/SavedStateWriter.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.savedstate
+
+/**
+ * An inline class that encapsulates an opaque [SavedState], and provides an API for writing the
+ * platform specific state.
+ *
+ * @see SavedState.write
+ */
+@JvmInline
+public expect value class SavedStateWriter
+internal constructor(
+ @PublishedApi internal val source: SavedState,
+) {
+
+ /**
+ * Stores a boolean value associated with the specified key in the [SavedState].
+ *
+ * @param key The key to associate the value with.
+ * @param value The boolean value to store.
+ */
+ public inline fun putBoolean(key: String, value: Boolean)
+
+ /**
+ * Stores a double value associated with the specified key in the [SavedState].
+ *
+ * @param key The key to associate the value with.
+ * @param value The double value to store.
+ */
+ public inline fun putDouble(key: String, value: Double)
+
+ /**
+ * Stores a float value associated with the specified key in the [SavedState].
+ *
+ * @param key The key to associate the value with.
+ * @param value The float value to store.
+ */
+ public inline fun putFloat(key: String, value: Float)
+
+ /**
+ * Stores an int value associated with the specified key in the [SavedState].
+ *
+ * @param key The key to associate the value with.
+ * @param value The int value to store.
+ */
+ public inline fun putInt(key: String, value: Int)
+
+ /**
+ * Stores a string value associated with the specified key in the [SavedState].
+ *
+ * @param key The key to associate the value with.
+ * @param value The string value to store.
+ */
+ public inline fun putString(key: String, value: String)
+
+ /**
+ * Stores a list of elements of [Int] associated with the specified key in the [SavedState].
+ *
+ * @param key The key to associate the value with.
+ * @param values The list of elements to store.
+ */
+ public inline fun putIntList(key: String, values: List<Int>)
+
+ /**
+ * Stores a list of elements of [String] associated with the specified key in the [SavedState].
+ *
+ * @param key The key to associate the value with.
+ * @param values The list of elements to store.
+ */
+ public inline fun putStringList(key: String, values: List<String>)
+
+ /**
+ * Stores a [SavedState] object associated with the specified key in the [SavedState].
+ *
+ * @param key The key to associate the value with.
+ * @param value The [SavedState] object to store
+ */
+ public inline fun putSavedState(key: String, value: SavedState)
+
+ /**
+ * Stores all key-value pairs from the provided [SavedState] into this [SavedState].
+ *
+ * @param values The [SavedState] containing the key-value pairs to add.
+ */
+ public inline fun putAll(values: SavedState)
+
+ /**
+ * Removes the value associated with the specified key from the [SavedState].
+ *
+ * @param key The key to remove.
+ */
+ public inline fun remove(key: String)
+
+ /** Removes all key-value pairs from the [SavedState]. */
+ public inline fun clear()
+}
diff --git a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/internal/SavedStateUtils.kt b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/internal/SavedStateUtils.kt
new file mode 100644
index 0000000..8cebe5e9
--- /dev/null
+++ b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/internal/SavedStateUtils.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.savedstate.internal
+
+@PublishedApi
+internal object SavedStateUtils {
+
+ const val DEFAULT_BOOLEAN = false
+ const val DEFAULT_FLOAT = 0f
+ const val DEFAULT_DOUBLE = 0.0
+ const val DEFAULT_INT = 0
+
+ @Suppress("NOTHING_TO_INLINE")
+ inline fun keyNotFoundError(key: String): Nothing =
+ error("Saved state key '$key' was not found")
+
+ inline fun <reified T> getValueFromSavedState(
+ key: String,
+ currentValue: () -> T?,
+ contains: (key: String) -> Boolean,
+ defaultValue: () -> T,
+ ): T {
+ return if (contains(key)) {
+ currentValue() ?: defaultValue()
+ } else {
+ defaultValue()
+ }
+ }
+}
diff --git a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/RobolectricTest.kt b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/RobolectricTest.kt
new file mode 100644
index 0000000..786c5fb
--- /dev/null
+++ b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/RobolectricTest.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.savedstate
+
+internal expect abstract class RobolectricTest()
diff --git a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateTest.kt b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateTest.kt
new file mode 100644
index 0000000..1034b38
--- /dev/null
+++ b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateTest.kt
@@ -0,0 +1,410 @@
+/*
+ * 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.savedstate
+
+import androidx.kruth.assertThat
+import androidx.kruth.assertThrows
+import androidx.savedstate.internal.SavedStateUtils
+import kotlin.test.Test
+
+internal class SavedStateTest : RobolectricTest() {
+
+ @Test
+ fun contains_whenHasKey_returnsTrue() {
+ val underTest = savedState { putInt(KEY_1, Int.MAX_VALUE) }
+
+ assertThat(underTest.read { contains(KEY_1) }).isTrue()
+ }
+
+ @Test
+ fun contains_whenDoesNotHaveKey_returnsFalse() {
+ val underTest = savedState { putInt(KEY_1, Int.MAX_VALUE) }
+
+ assertThat(underTest.read { contains(KEY_2) }).isFalse()
+ }
+
+ @Test
+ fun isEmpty_whenEmpty_returnTrue() {
+ val underTest = savedState()
+
+ assertThat(underTest.read { isEmpty() }).isTrue()
+ }
+
+ @Test
+ fun isEmpty_whenNotEmpty_returnFalse() {
+ val underTest = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ }
+
+ assertThat(underTest.read { isEmpty() }).isFalse()
+ }
+
+ @Test
+ fun size() {
+ val underTest = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ }
+
+ assertThat(underTest.read { size() }).isEqualTo(expected = 2)
+ }
+
+ @Test
+ fun remove() {
+ val underTest = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ }
+
+ underTest.read {
+ assertThat(contains(KEY_1)).isTrue()
+ assertThat(contains(KEY_2)).isTrue()
+ }
+
+ underTest.write { remove(KEY_1) }
+
+ underTest.read {
+ assertThat(contains(KEY_1)).isFalse()
+ assertThat(contains(KEY_2)).isTrue()
+ }
+ }
+
+ @Test
+ fun clear() {
+ val underTest = savedState {
+ putInt(KEY_1, Int.MAX_VALUE)
+ putInt(KEY_2, Int.MAX_VALUE)
+ }
+ underTest.write { clear() }
+
+ assertThat(underTest.read { isEmpty() }).isTrue()
+ }
+
+ // region getters and setters
+ @Test
+ fun getBoolean_whenSet_returns() {
+ val expected = true
+
+ val underTest = savedState { putBoolean(KEY_1, expected) }
+ val actual = underTest.read { getBoolean(KEY_1) }
+
+ assertThat(actual).isEqualTo(expected)
+ }
+
+ @Test
+ fun getBoolean_whenNotSet_throws() {
+ assertThrows<IllegalStateException> { savedState().read { getBoolean(KEY_1) } }
+ }
+
+ @Test
+ fun getBoolean_whenSet_differentType_returnsDefault() {
+ val underTest = savedState { putInt(KEY_1, Int.MAX_VALUE) }
+ val actual = underTest.read { getBoolean(KEY_1) }
+
+ assertThat(actual).isEqualTo(SavedStateUtils.DEFAULT_BOOLEAN)
+ }
+
+ @Test
+ fun getBooleanOrElse_whenSet_returns() {
+ val expected = true
+
+ val underTest = savedState { putBoolean(KEY_1, expected) }
+ val actual = underTest.read { getBooleanOrElse(KEY_1) { false } }
+
+ assertThat(actual).isEqualTo(expected)
+ }
+
+ @Test
+ fun getBooleanOrElse_whenNotSet_returnsElse() {
+ val actual = savedState().read { getBooleanOrElse(KEY_1) { false } }
+
+ assertThat(actual).isFalse()
+ }
+
+ @Test
+ fun getDouble_whenSet_returns() {
+ val underTest = savedState { putDouble(KEY_1, Double.MAX_VALUE) }
+ val actual = underTest.read { getDouble(KEY_1) }
+
+ assertThat(actual).isEqualTo(Double.MAX_VALUE)
+ }
+
+ @Test
+ fun getDouble_whenNotSet_throws() {
+ assertThrows<IllegalStateException> { savedState().read { getDouble(KEY_1) } }
+ }
+
+ @Test
+ fun getDouble_whenSet_differentType_returnsDefault() {
+ val underTest = savedState { putInt(KEY_1, Int.MAX_VALUE) }
+ val actual = underTest.read { getDouble(KEY_1) }
+
+ assertThat(actual).isEqualTo(SavedStateUtils.DEFAULT_DOUBLE)
+ }
+
+ @Test
+ fun getDoubleOrElse_whenSet_returns() {
+ val underTest = savedState { putDouble(KEY_1, Double.MAX_VALUE) }
+ val actual = underTest.read { getDoubleOrElse(KEY_1) { Double.MIN_VALUE } }
+
+ assertThat(actual).isEqualTo(Double.MAX_VALUE)
+ }
+
+ @Test
+ fun getDoubleOrElse_whenNotSet_returnsElse() {
+ val actual = savedState().read { getDoubleOrElse(KEY_1) { Double.MIN_VALUE } }
+
+ assertThat(actual).isEqualTo(Double.MIN_VALUE)
+ }
+
+ @Test
+ fun getFloat_whenSet_returns() {
+ val underTest = savedState { putFloat(KEY_1, Float.MAX_VALUE) }
+ val actual = underTest.read { getFloat(KEY_1) }
+
+ assertThat(actual).isEqualTo(Float.MAX_VALUE)
+ }
+
+ @Test
+ fun getFloat_whenNotSet_throws() {
+ assertThrows<IllegalStateException> { savedState().read { getFloat(KEY_1) } }
+ }
+
+ @Test
+ fun getFloat_whenSet_differentType_returnsDefault() {
+ val underTest = savedState { putInt(KEY_1, Int.MAX_VALUE) }
+ val actual = underTest.read { getFloat(KEY_1) }
+
+ assertThat(actual).isEqualTo(SavedStateUtils.DEFAULT_FLOAT)
+ }
+
+ @Test
+ fun getFloatOrElse_whenSet_returns() {
+ val underTest = savedState { putFloat(KEY_1, Float.MAX_VALUE) }
+ val actual = underTest.read { getFloatOrElse(KEY_1) { Float.MIN_VALUE } }
+
+ assertThat(actual).isEqualTo(Float.MAX_VALUE)
+ }
+
+ @Test
+ fun getFloatOrElse_whenNotSet_returnsElse() {
+ val actual = savedState().read { getFloatOrElse(KEY_1) { Float.MIN_VALUE } }
+
+ assertThat(actual).isEqualTo(Float.MIN_VALUE)
+ }
+
+ @Test
+ fun getInt_whenSet_returns() {
+ val underTest = savedState { putInt(KEY_1, Int.MAX_VALUE) }
+ val actual = underTest.read { getInt(KEY_1) }
+
+ assertThat(actual).isEqualTo(Int.MAX_VALUE)
+ }
+
+ @Test
+ fun getInt_whenNotSet_throws() {
+ assertThrows<IllegalStateException> { savedState().read { getInt(KEY_1) } }
+ }
+
+ @Test
+ fun getInt_whenSet_differentType_returnsDefault() {
+ val underTest = savedState { putBoolean(KEY_1, false) }
+ val actual = underTest.read { getInt(KEY_1) }
+
+ assertThat(actual).isEqualTo(SavedStateUtils.DEFAULT_INT)
+ }
+
+ @Test
+ fun getIntOrElse_whenSet_returns() {
+ val underTest = savedState { putInt(KEY_1, Int.MAX_VALUE) }
+ val actual = underTest.read { getIntOrElse(KEY_1) { Int.MIN_VALUE } }
+
+ assertThat(actual).isEqualTo(Int.MAX_VALUE)
+ }
+
+ @Test
+ fun getIntOrElse_whenNotSet_returnsElse() {
+ val actual = savedState().read { getIntOrElse(KEY_1) { Int.MIN_VALUE } }
+
+ assertThat(actual).isEqualTo(Int.MIN_VALUE)
+ }
+
+ @Test
+ fun getString_whenSet_returns() {
+ val underTest = savedState { putString(KEY_1, STRING_VALUE) }
+ val actual = underTest.read { getString(KEY_1) }
+
+ assertThat(actual).isEqualTo(STRING_VALUE)
+ }
+
+ @Test
+ fun getString_whenNotSet_throws() {
+ assertThrows<IllegalStateException> { savedState().read { getString(KEY_1) } }
+ }
+
+ @Test
+ fun getString_whenSet_differentType_throws() {
+ val underTest = savedState { putInt(KEY_1, Int.MAX_VALUE) }
+
+ assertThrows<IllegalStateException> { underTest.read { getString(KEY_1) } }
+ }
+
+ @Test
+ fun getStringOrElse_whenSet_returns() {
+ val underTest = savedState { putString(KEY_1, STRING_VALUE) }
+ val actual = underTest.read { getString(KEY_1) }
+
+ assertThat(actual).isEqualTo(STRING_VALUE)
+ }
+
+ @Test
+ fun getStringOrElse_whenNotSet_returnsElse() {
+ val actual = savedState().read { getStringOrElse(KEY_1) { STRING_VALUE } }
+
+ assertThat(actual).isEqualTo(STRING_VALUE)
+ }
+
+ @Test
+ fun getIntList_whenSet_returns() {
+ val expected = List(size = 5) { idx -> idx }
+
+ val underTest = savedState { putIntList(KEY_1, expected) }
+ val actual = underTest.read { getIntList(KEY_1) }
+
+ assertThat(actual).isEqualTo(expected)
+ }
+
+ @Test
+ fun getIntList_whenNotSet_throws() {
+ assertThrows<IllegalStateException> { savedState().read { getIntList(KEY_1) } }
+ }
+
+ @Test
+ fun getIntList_whenSet_differentType_throws() {
+ val expected = Int.MAX_VALUE
+
+ val underTest = savedState { putInt(KEY_1, expected) }
+
+ assertThrows<IllegalStateException> { underTest.read { getIntList(KEY_1) } }
+ }
+
+ @Test
+ fun getIntListOrElse_whenSet_returns() {
+ val underTest = savedState { putIntList(KEY_1, LIST_INT_VALUE) }
+ val actual = underTest.read { getIntListOrElse(KEY_1) { emptyList() } }
+
+ assertThat(actual).isEqualTo(LIST_INT_VALUE)
+ }
+
+ @Test
+ fun getIntListOrElse_whenNotSet_returnsElse() {
+ val actual = savedState().read { getIntListOrElse(KEY_1) { emptyList() } }
+
+ assertThat(actual).isEqualTo(emptyList<Int>())
+ }
+
+ @Test
+ fun getStringList_whenSet_returns() {
+ val underTest = savedState { putStringList(KEY_1, LIST_STRING_VALUE) }
+ val actual = underTest.read { getStringList(KEY_1) }
+
+ assertThat(actual).isEqualTo(LIST_STRING_VALUE)
+ }
+
+ @Test
+ fun getStringList_whenNotSet_throws() {
+ assertThrows<IllegalStateException> { savedState().read { getStringList(KEY_1) } }
+ }
+
+ @Test
+ fun getStringList_whenSet_differentType_throws() {
+ val expected = Int.MAX_VALUE
+
+ val underTest = savedState { putInt(KEY_1, expected) }
+
+ assertThrows<IllegalStateException> { underTest.read { getStringList(KEY_1) } }
+ }
+
+ @Test
+ fun getStringListOrElse_whenSet_returns() {
+ val underTest = savedState { putStringList(KEY_1, LIST_STRING_VALUE) }
+ val actual = underTest.read { getStringListOrElse(KEY_1) { emptyList() } }
+
+ assertThat(actual).isEqualTo(LIST_STRING_VALUE)
+ }
+
+ @Test
+ fun getStringListOrElse_whenNotSet_returnsElse() {
+ val actual = savedState().read { getStringListOrElse(KEY_1) { emptyList() } }
+
+ assertThat(actual).isEqualTo(emptyList<String>())
+ }
+
+ @Test
+ fun getSavedState_whenSet_returns() {
+ val underTest = savedState { putSavedState(KEY_1, SAVED_STATE_VALUE) }
+ val actual = underTest.read { getSavedState(KEY_1) }
+
+ assertThat(actual).isEqualTo(SAVED_STATE_VALUE)
+ }
+
+ @Test
+ fun getSavedState_whenNotSet_throws() {
+ assertThrows<IllegalStateException> { savedState().read { getSavedState(KEY_1) } }
+ }
+
+ @Test
+ fun getSavedStateOrElse_whenSet_returns() {
+ val underTest = savedState { putSavedState(KEY_1, SAVED_STATE_VALUE) }
+ val actual = underTest.read { getSavedStateOrElse(KEY_1) { savedState() } }
+
+ assertThat(actual).isEqualTo(SAVED_STATE_VALUE)
+ }
+
+ @Test
+ fun getSavedStateOrElse_whenNotSet_returnsElse() {
+ val expected = savedState()
+
+ val actual = savedState().read { getSavedStateOrElse(KEY_1) { expected } }
+
+ assertThat(actual).isEqualTo(expected)
+ }
+
+ @Test
+ fun putAll() {
+ val previousState = savedState { putInt(KEY_1, Int.MAX_VALUE) }
+
+ val underTest = savedState { putAll(previousState) }
+ val actual = underTest.read { getInt(KEY_1) }
+
+ assertThat(actual).isEqualTo(Int.MAX_VALUE)
+ }
+
+ // endregion
+
+ private companion object {
+ const val KEY_1 = "KEY_1"
+ const val KEY_2 = "KEY_2"
+ const val STRING_VALUE = "string-value"
+ val LIST_INT_VALUE = List(size = 5) { idx -> idx }
+ val LIST_STRING_VALUE = List(size = 5) { idx -> "index=$idx" }
+ val SET_INT_VALUE = LIST_INT_VALUE.toSet()
+ val SET_STRING_VALUE = LIST_STRING_VALUE.toSet()
+ val SAVED_STATE_VALUE = savedState()
+ }
+}
diff --git a/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedState.nonAndroid.kt b/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedState.nonAndroid.kt
new file mode 100644
index 0000000..127e516
--- /dev/null
+++ b/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedState.nonAndroid.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.savedstate
+
+public actual class SavedState
+@PublishedApi
+internal constructor(@PublishedApi internal val map: MutableMap<String, Any> = mutableMapOf())
+
+actual inline fun savedState(block: SavedStateWriter.() -> Unit): SavedState =
+ SavedState().apply { write(block) }
diff --git a/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedStateReader.nonAndroid.kt b/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedStateReader.nonAndroid.kt
new file mode 100644
index 0000000..aabc6e8
--- /dev/null
+++ b/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedStateReader.nonAndroid.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.savedstate
+
+import androidx.savedstate.internal.SavedStateUtils
+import androidx.savedstate.internal.SavedStateUtils.getValueFromSavedState
+import androidx.savedstate.internal.SavedStateUtils.keyNotFoundError
+
+@Suppress("NOTHING_TO_INLINE")
+@JvmInline
+actual value class SavedStateReader actual constructor(actual val source: SavedState) {
+
+ actual inline fun getBoolean(key: String): Boolean =
+ getSingleResultOrThrow(key) {
+ source.map[key] as? Boolean ?: SavedStateUtils.DEFAULT_BOOLEAN
+ }
+
+ actual inline fun getBooleanOrElse(key: String, defaultValue: () -> Boolean): Boolean =
+ getSingleResultOrElse(key, defaultValue) {
+ source.map[key] as? Boolean ?: SavedStateUtils.DEFAULT_BOOLEAN
+ }
+
+ actual inline fun getDouble(key: String): Double =
+ getSingleResultOrThrow(key) { source.map[key] as? Double ?: SavedStateUtils.DEFAULT_DOUBLE }
+
+ actual inline fun getDoubleOrElse(key: String, defaultValue: () -> Double): Double =
+ getSingleResultOrElse(key, defaultValue) {
+ source.map[key] as? Double ?: SavedStateUtils.DEFAULT_DOUBLE
+ }
+
+ actual inline fun getFloat(key: String): Float =
+ getSingleResultOrThrow(key) { source.map[key] as? Float ?: SavedStateUtils.DEFAULT_FLOAT }
+
+ actual inline fun getFloatOrElse(key: String, defaultValue: () -> Float): Float =
+ getSingleResultOrElse(key, defaultValue) {
+ source.map[key] as? Float ?: SavedStateUtils.DEFAULT_FLOAT
+ }
+
+ actual inline fun getInt(key: String): Int =
+ getSingleResultOrThrow(key) { source.map[key] as? Int ?: SavedStateUtils.DEFAULT_INT }
+
+ actual inline fun getIntOrElse(key: String, defaultValue: () -> Int): Int =
+ getSingleResultOrElse(key, defaultValue) {
+ source.map[key] as? Int ?: SavedStateUtils.DEFAULT_INT
+ }
+
+ actual inline fun getString(key: String): String =
+ getSingleResultOrThrow(key) { source.map[key] as? String }
+
+ actual inline fun getStringOrElse(key: String, defaultValue: () -> String): String =
+ getSingleResultOrElse(key, defaultValue) { source.map[key] as? String }
+
+ @Suppress("UNCHECKED_CAST")
+ actual inline fun getIntList(key: String): List<Int> {
+ return getListResultOrThrow(key) { source.map[key] as? List<Int> }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ actual inline fun getIntListOrElse(key: String, defaultValue: () -> List<Int>): List<Int> {
+ return getListResultOrElse(key, defaultValue) { source.map[key] as? List<Int> }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ actual inline fun getStringList(key: String): List<String> {
+ return getListResultOrThrow(key) { source.map[key] as? List<String> }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ actual inline fun getStringListOrElse(
+ key: String,
+ defaultValue: () -> List<String>
+ ): List<String> {
+ return getListResultOrElse(key, defaultValue) { source.map[key] as? List<String> }
+ }
+
+ actual inline fun getSavedState(key: String): SavedState =
+ getSingleResultOrThrow(key) { source.map[key] as? SavedState }
+
+ actual inline fun getSavedStateOrElse(key: String, defaultValue: () -> SavedState): SavedState =
+ getSingleResultOrElse(key, defaultValue) { source.map[key] as? SavedState }
+
+ actual inline fun size(): Int {
+ return source.map.size
+ }
+
+ actual inline fun isEmpty(): Boolean {
+ return source.map.isEmpty()
+ }
+
+ actual inline operator fun contains(key: String): Boolean {
+ return source.map.containsKey(key)
+ }
+
+ @PublishedApi
+ internal inline fun <reified T> getSingleResultOrThrow(
+ key: String,
+ currentValue: () -> T?,
+ ): T =
+ getValueFromSavedState(
+ key = key,
+ contains = { source.map.containsKey(key) },
+ currentValue = { currentValue() },
+ defaultValue = { keyNotFoundError(key) },
+ )
+
+ @PublishedApi
+ internal inline fun <reified T> getSingleResultOrElse(
+ key: String,
+ defaultValue: () -> T,
+ currentValue: () -> T?,
+ ): T =
+ getValueFromSavedState(
+ key = key,
+ contains = { source.map.containsKey(key) },
+ currentValue = { currentValue() },
+ defaultValue = { defaultValue() },
+ )
+
+ @PublishedApi
+ internal inline fun <reified T> getListResultOrThrow(
+ key: String,
+ currentValue: () -> List<T>?,
+ ): List<T> =
+ getValueFromSavedState(
+ key = key,
+ contains = { source.map.containsKey(key) },
+ currentValue = { currentValue() },
+ defaultValue = { keyNotFoundError(key) },
+ )
+
+ @PublishedApi
+ internal inline fun <reified T> getListResultOrElse(
+ key: String,
+ defaultValue: () -> List<T>,
+ currentValue: () -> List<T>?,
+ ): List<T> =
+ getValueFromSavedState(
+ key = key,
+ contains = { source.map.containsKey(key) },
+ currentValue = { currentValue() },
+ defaultValue = { defaultValue() },
+ )
+}
diff --git a/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedStateWriter.nonAndroid.kt b/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedStateWriter.nonAndroid.kt
new file mode 100644
index 0000000..dbc41dd
--- /dev/null
+++ b/savedstate/savedstate/src/nonAndroidMain/kotlin/androidx/savedstate/SavedStateWriter.nonAndroid.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.savedstate
+
+@Suppress("NOTHING_TO_INLINE")
+@JvmInline
+actual value class SavedStateWriter actual constructor(actual val source: SavedState) {
+
+ actual inline fun putBoolean(key: String, value: Boolean) {
+ source.map[key] = value
+ }
+
+ actual inline fun putDouble(key: String, value: Double) {
+ source.map[key] = value
+ }
+
+ actual inline fun putFloat(key: String, value: Float) {
+ source.map[key] = value
+ }
+
+ actual inline fun putInt(key: String, value: Int) {
+ source.map[key] = value
+ }
+
+ actual inline fun putString(key: String, value: String) {
+ source.map[key] = value
+ }
+
+ actual inline fun putSavedState(key: String, value: SavedState) {
+ source.map[key] = value
+ }
+
+ actual inline fun putIntList(key: String, values: List<Int>) {
+ source.map[key] = values
+ }
+
+ actual inline fun putStringList(key: String, values: List<String>) {
+ source.map[key] = values
+ }
+
+ actual inline fun putAll(values: SavedState) {
+ source.map.putAll(values.map)
+ }
+
+ actual inline fun remove(key: String) {
+ source.map.remove(key)
+ }
+
+ actual inline fun clear() {
+ source.map.clear()
+ }
+}
diff --git a/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/RobolectricTest.nonAndroid.kt b/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/RobolectricTest.nonAndroid.kt
new file mode 100644
index 0000000..d72a1d9
--- /dev/null
+++ b/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/RobolectricTest.nonAndroid.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.savedstate
+
+internal actual abstract class RobolectricTest actual constructor()
diff --git a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
index f63dae9..dd6a03a 100644
--- a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
+++ b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
@@ -393,7 +393,7 @@
}
val cvePattern = Pattern.compile("CVE-\\d{4}-\\d{4,}")
- val asbPattern = Pattern.compile("ASB-A-\\d{4,}")
+ val asbPattern = Pattern.compile("(ASB|PUB)-A-\\d{4,}")
result.vulnerabilities.values.flatten().forEach { group ->
group.cveIdentifiers.forEach { cve ->
@@ -407,7 +407,7 @@
group.asbIdentifiers.forEach { asb ->
if (!asbPattern.matcher(asb).matches()) {
throw IllegalArgumentException(
- "ASB identifier does not match the required format (ASB-A-XXXX): $asb"
+ "ASB identifier $asb does not match the required format: $asbPattern"
)
}
}
diff --git a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
index ba905ac..0df63be 100644
--- a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
+++ b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
@@ -175,6 +175,12 @@
"asb_identifiers": ["ASB-A-2020"],
"severity": "high",
"components": ["system", "vendor"]
+ }],
+ "2020-05-01": [{
+ "cve_identifiers": ["CVE-2020-5678"],
+ "asb_identifiers": ["PUB-A-5678"],
+ "severity": "moderate",
+ "components": ["system"]
}]
},
"extra_field": { test: 12345 },
@@ -192,7 +198,31 @@
)
assertEquals(1, fixes[SecurityPatchState.Severity.HIGH]?.size)
+ assertEquals(1, fixes[SecurityPatchState.Severity.MODERATE]?.size)
assertEquals(setOf("CVE-2020-1234"), fixes[SecurityPatchState.Severity.HIGH])
+ assertEquals(setOf("CVE-2020-5678"), fixes[SecurityPatchState.Severity.MODERATE])
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun testParseVulnerabilityReport_invalidAsb_throwsIllegalArgumentException() {
+ val jsonString =
+ """
+ {
+ "vulnerabilities": {
+ "2020-01-01": [{
+ "cve_identifiers": ["CVE-2020-1234"],
+ "asb_identifiers": ["ASB-123"],
+ "severity": "high",
+ "components": ["system", "vendor"]
+ }]
+ },
+ "extra_field": { test: 12345 },
+ "kernel_lts_versions": {
+ "2020-01-01": ["4.14"]
+ }
+ }
+ """
+ securityState.loadVulnerabilityReport(jsonString)
}
@Test(expected = IllegalArgumentException::class)
diff --git a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
index 05a1e4b..649b4ba 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
+++ b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
@@ -126,6 +126,201 @@
}
@Test
+ fun multipleReplaceOperationFastSystemBack() {
+ withUse(ActivityScenario.launch(FragmentTransitionTestActivity::class.java)) {
+ val fm1 = withActivity { supportFragmentManager }
+
+ val fragment1 = TransitionFragment(R.layout.scene1)
+ fragment1.setReenterTransition(Fade().apply { duration = 300 })
+ fragment1.setReturnTransition(Fade().apply { duration = 300 })
+
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment1, "1")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ waitForExecution()
+
+ val fragment2 = TransitionFragment(R.layout.scene1)
+ fragment2.setReenterTransition(Fade().apply { duration = 300 })
+ fragment2.setReturnTransition(Fade().apply { duration = 300 })
+
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2, "2")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ waitForExecution()
+
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+
+ val fragment3 = TransitionFragment(R.layout.scene1)
+ fragment3.setReenterTransition(Fade().apply { duration = 300 })
+ fragment3.setReturnTransition(Fade().apply { duration = 300 })
+
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment3, "3")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ waitForExecution()
+
+ fragment2.waitForTransition()
+ fragment3.waitForTransition()
+
+ val fragment4 = TransitionFragment(R.layout.scene1)
+ fragment4.setReenterTransition(Fade().apply { duration = 300 })
+ fragment4.setReturnTransition(Fade().apply { duration = 300 })
+
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment4, "3")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ waitForExecution()
+
+ fragment3.waitForTransition()
+ fragment4.waitForTransition()
+
+ val dispatcher = withActivity { onBackPressedDispatcher }
+ withActivity {
+ dispatcher.dispatchOnBackStarted(
+ BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT)
+ )
+ }
+ withActivity { dispatcher.onBackPressed() }
+ waitForExecution()
+
+ withActivity {
+ dispatcher.dispatchOnBackStarted(
+ BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT)
+ )
+ }
+ withActivity { dispatcher.onBackPressed() }
+ waitForExecution()
+
+ withActivity {
+ dispatcher.dispatchOnBackStarted(
+ BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT)
+ )
+ }
+ withActivity { dispatcher.onBackPressed() }
+ waitForExecution()
+
+ fragment1.waitForNoTransition()
+
+ assertThat(fragment2.isAdded).isFalse()
+ assertThat(fm1.findFragmentByTag("2")).isEqualTo(null)
+
+ // Make sure the original fragment was correctly readded to the container
+ assertThat(fragment1.requireView().parent).isNotNull()
+ }
+ }
+
+ @Test
+ fun multipleReplaceOperationFastGestureBack() {
+ withUse(ActivityScenario.launch(FragmentTransitionTestActivity::class.java)) {
+ val fm1 = withActivity { supportFragmentManager }
+
+ val fragment1 = TransitionFragment(R.layout.scene1)
+ fragment1.setReenterTransition(Fade().apply { duration = 300 })
+ fragment1.setReturnTransition(Fade().apply { duration = 300 })
+
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment1, "1")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ waitForExecution()
+
+ val fragment2 = TransitionFragment(R.layout.scene1)
+ fragment2.setReenterTransition(Fade().apply { duration = 300 })
+ fragment2.setReturnTransition(Fade().apply { duration = 300 })
+
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2, "2")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ waitForExecution()
+
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+
+ val fragment3 = TransitionFragment(R.layout.scene1)
+ fragment3.setReenterTransition(Fade().apply { duration = 300 })
+ fragment3.setReturnTransition(Fade().apply { duration = 300 })
+
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment3, "3")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ waitForExecution()
+
+ fragment2.waitForTransition()
+ fragment3.waitForTransition()
+
+ val fragment4 = TransitionFragment(R.layout.scene1)
+ fragment4.setReenterTransition(Fade().apply { duration = 300 })
+ fragment4.setReturnTransition(Fade().apply { duration = 300 })
+
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment4, "3")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ waitForExecution()
+
+ fragment3.waitForTransition()
+ fragment4.waitForTransition()
+
+ val dispatcher = withActivity { onBackPressedDispatcher }
+ withActivity {
+ dispatcher.dispatchOnBackStarted(
+ BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT)
+ )
+ dispatcher.dispatchOnBackProgressed(
+ BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+ )
+ }
+ withActivity { dispatcher.onBackPressed() }
+ waitForExecution()
+
+ withActivity {
+ dispatcher.dispatchOnBackStarted(
+ BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT)
+ )
+ dispatcher.dispatchOnBackProgressed(
+ BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+ )
+ }
+ withActivity { dispatcher.onBackPressed() }
+ waitForExecution()
+
+ withActivity {
+ dispatcher.dispatchOnBackStarted(
+ BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT)
+ )
+ dispatcher.dispatchOnBackProgressed(
+ BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+ )
+ }
+ withActivity { dispatcher.onBackPressed() }
+ waitForExecution()
+
+ fragment1.waitForNoTransition()
+
+ assertThat(fragment2.isAdded).isFalse()
+ assertThat(fm1.findFragmentByTag("2")).isEqualTo(null)
+
+ // Make sure the original fragment was correctly readded to the container
+ assertThat(fragment1.requireView().parent).isNotNull()
+ }
+ }
+
+ @Test
fun replaceOperationWithTransitionsThenBackCancelled() {
withUse(ActivityScenario.launch(FragmentTransitionTestActivity::class.java)) {
val fm1 = withActivity { supportFragmentManager }
@@ -141,7 +336,7 @@
startedEnterCountDownLatch.countDown()
}
- override fun onTransitionEnd(transition: Transition) {
+ override fun onTransitionCancel(transition: Transition) {
transitionEndCountDownLatch.countDown()
}
}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
index c1c7e8e..26080e1 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
@@ -266,9 +266,12 @@
if (targetValue != RevealValue.Covered) {
resetLastState(this)
}
- swipeableState.animateTo(targetValue)
- if (targetValue == RevealValue.Covered) {
- lastActionType = RevealActionType.None
+ try {
+ swipeableState.animateTo(targetValue)
+ } finally {
+ if (targetValue == RevealValue.Covered) {
+ lastActionType = RevealActionType.None
+ }
}
}
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index ce8c266..e6fff1a 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -918,7 +918,7 @@
public final class SegmentedCircularProgressIndicatorKt {
method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
- method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> completed, optional androidx.compose.ui.Modifier modifier, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
+ method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> segmentValue, optional androidx.compose.ui.Modifier modifier, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
}
public final class ShapeDefaults {
@@ -1172,6 +1172,31 @@
method @androidx.compose.runtime.Composable public static void SwipeToDismissBox(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissed, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.SwipeToDismissBoxState state, optional long backgroundScrimColor, optional long contentScrimColor, optional Object backgroundKey, optional Object contentKey, optional boolean userSwipeEnabled, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Boolean,kotlin.Unit> content);
}
+ public final class SwipeToRevealDefaults {
+ method public float getDoubleActionAnchorWidth();
+ method public float getLargeActionButtonHeight();
+ method public float getSingleActionAnchorWidth();
+ method public float getSmallActionButtonHeight();
+ property public final float DoubleActionAnchorWidth;
+ property public final float LargeActionButtonHeight;
+ property public final float SingleActionAnchorWidth;
+ property public final float SmallActionButtonHeight;
+ field public static final androidx.wear.compose.material3.SwipeToRevealDefaults INSTANCE;
+ }
+
+ public final class SwipeToRevealKt {
+ method @androidx.compose.runtime.Composable public static void SwipeToReveal(kotlin.jvm.functions.Function1<? super androidx.wear.compose.material3.SwipeToRevealScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.RevealState revealState, optional float actionButtonHeight, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.RevealState rememberRevealState(optional int initialValue, optional float anchorWidth, optional boolean useAnchoredActions);
+ }
+
+ public final class SwipeToRevealScope {
+ ctor public SwipeToRevealScope();
+ method public void primaryAction(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, String label, optional long containerColor, optional long contentColor);
+ method public void secondaryAction(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, String label, optional long containerColor, optional long contentColor);
+ method public void undoPrimaryAction(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, String label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional long containerColor, optional long contentColor);
+ method public void undoSecondaryAction(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, String label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional long containerColor, optional long contentColor);
+ }
+
@androidx.compose.runtime.Immutable public final class SwitchButtonColors {
ctor public SwitchButtonColors(long checkedContainerColor, long checkedContentColor, long checkedSecondaryContentColor, long checkedIconColor, long checkedThumbColor, long checkedThumbIconColor, long checkedTrackBorderColor, long checkedTrackColor, long uncheckedContainerColor, long uncheckedContentColor, long uncheckedSecondaryContentColor, long uncheckedIconColor, long uncheckedThumbColor, long uncheckedTrackColor, long uncheckedTrackBorderColor, long disabledCheckedContainerColor, long disabledCheckedContentColor, long disabledCheckedSecondaryContentColor, long disabledCheckedIconColor, long disabledCheckedThumbColor, long disabledCheckedThumbIconColor, long disabledCheckedTrackColor, long disabledCheckedTrackBorderColor, long disabledUncheckedContainerColor, long disabledUncheckedContentColor, long disabledUncheckedSecondaryContentColor, long disabledUncheckedIconColor, long disabledUncheckedThumbColor, long disabledUncheckedTrackBorderColor);
method public long getCheckedContainerColor();
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index ce8c266..e6fff1a 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -918,7 +918,7 @@
public final class SegmentedCircularProgressIndicatorKt {
method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
- method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> completed, optional androidx.compose.ui.Modifier modifier, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
+ method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> segmentValue, optional androidx.compose.ui.Modifier modifier, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
}
public final class ShapeDefaults {
@@ -1172,6 +1172,31 @@
method @androidx.compose.runtime.Composable public static void SwipeToDismissBox(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissed, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.SwipeToDismissBoxState state, optional long backgroundScrimColor, optional long contentScrimColor, optional Object backgroundKey, optional Object contentKey, optional boolean userSwipeEnabled, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Boolean,kotlin.Unit> content);
}
+ public final class SwipeToRevealDefaults {
+ method public float getDoubleActionAnchorWidth();
+ method public float getLargeActionButtonHeight();
+ method public float getSingleActionAnchorWidth();
+ method public float getSmallActionButtonHeight();
+ property public final float DoubleActionAnchorWidth;
+ property public final float LargeActionButtonHeight;
+ property public final float SingleActionAnchorWidth;
+ property public final float SmallActionButtonHeight;
+ field public static final androidx.wear.compose.material3.SwipeToRevealDefaults INSTANCE;
+ }
+
+ public final class SwipeToRevealKt {
+ method @androidx.compose.runtime.Composable public static void SwipeToReveal(kotlin.jvm.functions.Function1<? super androidx.wear.compose.material3.SwipeToRevealScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.RevealState revealState, optional float actionButtonHeight, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.RevealState rememberRevealState(optional int initialValue, optional float anchorWidth, optional boolean useAnchoredActions);
+ }
+
+ public final class SwipeToRevealScope {
+ ctor public SwipeToRevealScope();
+ method public void primaryAction(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, String label, optional long containerColor, optional long contentColor);
+ method public void secondaryAction(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, String label, optional long containerColor, optional long contentColor);
+ method public void undoPrimaryAction(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, String label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional long containerColor, optional long contentColor);
+ method public void undoSecondaryAction(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, String label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional long containerColor, optional long contentColor);
+ }
+
@androidx.compose.runtime.Immutable public final class SwitchButtonColors {
ctor public SwitchButtonColors(long checkedContainerColor, long checkedContentColor, long checkedSecondaryContentColor, long checkedIconColor, long checkedThumbColor, long checkedThumbIconColor, long checkedTrackBorderColor, long checkedTrackColor, long uncheckedContainerColor, long uncheckedContentColor, long uncheckedSecondaryContentColor, long uncheckedIconColor, long uncheckedThumbColor, long uncheckedTrackColor, long uncheckedTrackBorderColor, long disabledCheckedContainerColor, long disabledCheckedContentColor, long disabledCheckedSecondaryContentColor, long disabledCheckedIconColor, long disabledCheckedThumbColor, long disabledCheckedThumbIconColor, long disabledCheckedTrackColor, long disabledCheckedTrackBorderColor, long disabledUncheckedContainerColor, long disabledUncheckedContentColor, long disabledUncheckedSecondaryContentColor, long disabledUncheckedIconColor, long disabledUncheckedThumbColor, long disabledUncheckedTrackBorderColor);
method public long getCheckedContainerColor();
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
index 88c23e1..1bf97f8 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
@@ -54,7 +54,7 @@
import androidx.wear.compose.material3.samples.LinearProgressIndicatorSample
import androidx.wear.compose.material3.samples.MediaButtonProgressIndicatorSample
import androidx.wear.compose.material3.samples.OverflowProgressIndicatorSample
-import androidx.wear.compose.material3.samples.SegmentedProgressIndicatorOnOffSample
+import androidx.wear.compose.material3.samples.SegmentedProgressIndicatorBinarySample
import androidx.wear.compose.material3.samples.SegmentedProgressIndicatorSample
import androidx.wear.compose.material3.samples.SmallSegmentedProgressIndicatorSample
import androidx.wear.compose.material3.samples.SmallValuesProgressIndicatorSample
@@ -93,8 +93,8 @@
Centralize { IndeterminateProgressIndicatorSample() }
},
ComposableDemo("Segmented progress") { Centralize { SegmentedProgressIndicatorSample() } },
- ComposableDemo("Progress segments on/off") {
- Centralize { SegmentedProgressIndicatorOnOffSample() }
+ ComposableDemo("Segmented binary") {
+ Centralize { SegmentedProgressIndicatorBinarySample() }
},
ComposableDemo("Small segmented progress") {
Centralize { SmallSegmentedProgressIndicatorSample() }
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwipeToRevealDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwipeToRevealDemo.kt
new file mode 100644
index 0000000..0c777e61
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwipeToRevealDemo.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.wear.compose.material3.demos
+
+import android.widget.Toast
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+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.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
+import androidx.wear.compose.foundation.RevealActionType
+import androidx.wear.compose.foundation.RevealValue
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Card
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.SplitSwitchButton
+import androidx.wear.compose.material3.SwipeToReveal
+import androidx.wear.compose.material3.SwipeToRevealDefaults
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.rememberRevealState
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalWearFoundationApi::class)
+@Composable
+fun SwipeToRevealTwoActionsWithUndo() {
+ val context = LocalContext.current
+ val showToasts = remember { mutableStateOf(true) }
+
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ SwipeToReveal(
+ // Use the double action anchor width when revealing two actions
+ revealState =
+ rememberRevealState(anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth),
+ actionButtonHeight = SwipeToRevealDefaults.LargeActionButtonHeight,
+ actions = {
+ primaryAction(
+ onClick = {
+ if (showToasts.value) {
+ Toast.makeText(context, "Primary action executed.", Toast.LENGTH_SHORT)
+ .show()
+ }
+ },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ label = "Delete"
+ )
+ secondaryAction(
+ onClick = {
+ if (showToasts.value) {
+ Toast.makeText(
+ context,
+ "Secondary action executed.",
+ Toast.LENGTH_SHORT
+ )
+ .show()
+ }
+ },
+ icon = { Icon(Icons.Filled.Lock, contentDescription = "Lock") },
+ label = "Lock"
+ )
+ undoPrimaryAction(
+ onClick = {
+ if (showToasts.value) {
+ Toast.makeText(
+ context,
+ "Undo primary action executed.",
+ Toast.LENGTH_SHORT
+ )
+ .show()
+ }
+ },
+ label = "Undo Delete"
+ )
+ undoSecondaryAction(
+ onClick = {
+ if (showToasts.value) {
+ Toast.makeText(
+ context,
+ "Undo secondary action executed.",
+ Toast.LENGTH_SHORT
+ )
+ .show()
+ }
+ },
+ label = "Undo Lock"
+ )
+ }
+ ) {
+ Card(modifier = Modifier.fillMaxWidth(), onClick = {}) {
+ Text("This Card has two actions", modifier = Modifier.fillMaxSize())
+ }
+ }
+ Spacer(Modifier.size(4.dp))
+ SplitSwitchButton(
+ showToasts.value,
+ onCheckedChange = { showToasts.value = it },
+ onContainerClick = { showToasts.value = !showToasts.value },
+ toggleContentDescription = "Show toasts"
+ ) {
+ Text("Show toasts")
+ }
+ }
+}
+
+@OptIn(ExperimentalWearFoundationApi::class)
+@Composable
+fun SwipeToRevealInList() {
+ val namesList = remember { mutableStateListOf("Alice", "Bob", "Charlie", "Dave", "Eve") }
+ val coroutineScope = rememberCoroutineScope()
+ ScalingLazyColumn(contentPadding = PaddingValues(0.dp)) {
+ items(namesList.size, key = { namesList[it] }) {
+ val revealState =
+ rememberRevealState(anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth)
+ val name = remember { namesList[it] }
+ SwipeToReveal(
+ revealState = revealState,
+ actions = {
+ primaryAction(
+ onClick = {
+ coroutineScope.launch {
+ delay(2000)
+ // After a delay, remove the item from the list if the last action
+ // performed by the user is still the primary action, so the user
+ // didn't press "Undo".
+ if (revealState.lastActionType == RevealActionType.PrimaryAction) {
+ namesList.remove(name)
+ }
+ }
+ },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ label = "Delete"
+ )
+ secondaryAction(
+ onClick = {
+ // Add a duplicate item to the list, if it doesn't exist already
+ val nextName = "$name+"
+ if (!namesList.contains(nextName)) {
+ namesList.add(namesList.indexOf(name) + 1, nextName)
+ coroutineScope.launch { revealState.animateTo(RevealValue.Covered) }
+ }
+ },
+ icon = { Icon(Icons.Filled.Add, contentDescription = "Duplicate") },
+ label = "Duplicate"
+ )
+ undoPrimaryAction(onClick = {}, label = "Undo Delete")
+ }
+ ) {
+ Button({}, Modifier.fillMaxWidth().padding(horizontal = 4.dp)) { Text(name) }
+ }
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index 5e2a401..fb753ac 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -38,6 +38,9 @@
import androidx.wear.compose.material3.samples.StepperSample
import androidx.wear.compose.material3.samples.StepperWithIntegerSample
import androidx.wear.compose.material3.samples.StepperWithRangeSemanticsSample
+import androidx.wear.compose.material3.samples.SwipeToRevealNonAnchoredSample
+import androidx.wear.compose.material3.samples.SwipeToRevealSample
+import androidx.wear.compose.material3.samples.SwipeToRevealSingleActionCardSample
val WearMaterial3Demos =
Material3DemoCategory(
@@ -154,6 +157,23 @@
)
),
Material3DemoCategory(
+ title = "Swipe to Reveal",
+ listOf(
+ ComposableDemo("Two Actions") { Centralize { SwipeToRevealSample() } },
+ ComposableDemo("Two Undo Actions") {
+ Centralize { SwipeToRevealTwoActionsWithUndo() }
+ },
+ ComposableDemo("Single action with Card") {
+ Centralize { SwipeToRevealSingleActionCardSample() }
+ },
+ ComposableDemo("In a list") { Centralize { SwipeToRevealInList() } },
+ ComposableDemo("Non-anchoring") {
+ Centralize { SwipeToRevealNonAnchoredSample() }
+ }
+ )
+ ),
+ Material3DemoCategory(title = "Typography", TypographyDemos),
+ Material3DemoCategory(
"Animated Text",
if (Build.VERSION.SDK_INT > 31) {
listOf(
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
index 47a6bd2..90c6d1f 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
@@ -169,7 +169,7 @@
@Sampled
@Composable
-fun SegmentedProgressIndicatorOnOffSample() {
+fun SegmentedProgressIndicatorBinarySample() {
Box(
modifier =
Modifier.background(MaterialTheme.colorScheme.background)
@@ -178,7 +178,7 @@
) {
SegmentedCircularProgressIndicator(
segmentCount = 5,
- completed = { it % 2 != 0 },
+ segmentValue = { it % 2 != 0 },
)
}
}
@@ -189,7 +189,7 @@
Box(modifier = Modifier.fillMaxSize()) {
SegmentedCircularProgressIndicator(
segmentCount = 8,
- completed = { it % 2 != 0 },
+ segmentValue = { it % 2 != 0 },
modifier = Modifier.align(Alignment.Center).size(80.dp)
)
}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/SwipeToRevealSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/SwipeToRevealSample.kt
new file mode 100644
index 0000000..7e5ccbb
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/SwipeToRevealSample.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.MoreVert
+import androidx.compose.material.icons.outlined.Refresh
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Card
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.SwipeToReveal
+import androidx.wear.compose.material3.SwipeToRevealDefaults
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.rememberRevealState
+
+@OptIn(ExperimentalWearFoundationApi::class)
+@Composable
+@Sampled
+fun SwipeToRevealSample() {
+ SwipeToReveal(
+ // Use the double action anchor width when revealing two actions
+ revealState =
+ rememberRevealState(anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth),
+ actions = {
+ primaryAction(
+ onClick = { /* This block is called when the primary action is executed. */ },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ label = "Delete"
+ )
+ secondaryAction(
+ onClick = { /* This block is called when the secondary action is executed. */ },
+ icon = { Icon(Icons.Outlined.MoreVert, contentDescription = "Options") },
+ label = "Options"
+ )
+ undoPrimaryAction(
+ onClick = { /* This block is called when the undo primary action is executed. */ },
+ label = "Undo Delete"
+ )
+ }
+ ) {
+ Button(modifier = Modifier.fillMaxWidth(), onClick = {}) {
+ Text("This Button has two actions", modifier = Modifier.fillMaxSize())
+ }
+ }
+}
+
+@OptIn(ExperimentalWearFoundationApi::class)
+@Composable
+@Sampled
+fun SwipeToRevealSingleActionCardSample() {
+ SwipeToReveal(
+ actionButtonHeight = SwipeToRevealDefaults.LargeActionButtonHeight,
+ actions = {
+ primaryAction(
+ onClick = { /* This block is called when the primary action is executed. */ },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ label = "Delete"
+ )
+ }
+ ) {
+ Card(modifier = Modifier.fillMaxWidth(), onClick = {}) {
+ Text(
+ "This Card has one action, and the revealed button is taller",
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalWearFoundationApi::class)
+@Composable
+@Sampled
+fun SwipeToRevealNonAnchoredSample() {
+ SwipeToReveal(
+ revealState = rememberRevealState(useAnchoredActions = false),
+ actions = {
+ primaryAction(
+ onClick = { /* This block is called when the primary action is executed. */ },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ label = "Delete"
+ )
+ undoPrimaryAction(
+ onClick = { /* This block is called when the undo primary action is executed. */ },
+ icon = { Icon(Icons.Outlined.Refresh, contentDescription = "Undo") },
+ label = "Undo"
+ )
+ }
+ ) {
+ Button(modifier = Modifier.fillMaxWidth(), onClick = {}) {
+ Text("Swipe to execute the primary action.", modifier = Modifier.fillMaxSize())
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt
index 66b4f3a..2d74abc 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt
@@ -207,7 +207,7 @@
verifyProgressIndicatorScreenshot(screenSize = screenSize) {
SegmentedCircularProgressIndicator(
segmentCount = 6,
- completed = { it % 2 == 0 },
+ segmentValue = { it % 2 == 0 },
modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
startAngle = 120f,
endAngle = 60f,
@@ -219,7 +219,7 @@
verifyProgressIndicatorScreenshot(screenSize = screenSize) {
SegmentedCircularProgressIndicator(
segmentCount = 6,
- completed = { it % 2 == 0 },
+ segmentValue = { it % 2 == 0 },
modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
startAngle = 120f,
endAngle = 60f,
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SegmentedCircularProgressIndicatorTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SegmentedCircularProgressIndicatorTest.kt
index 2182465..a301dbe 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SegmentedCircularProgressIndicatorTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SegmentedCircularProgressIndicatorTest.kt
@@ -214,7 +214,7 @@
setContentWithTheme {
SegmentedCircularProgressIndicator(
segmentCount = 6,
- completed = { it % 2 != 0 },
+ segmentValue = { it % 2 != 0 },
modifier = Modifier.testTag(TEST_TAG),
colors =
ProgressIndicatorDefaults.colors(
@@ -243,7 +243,7 @@
setContentWithTheme {
SegmentedCircularProgressIndicator(
segmentCount = 6,
- completed = { true },
+ segmentValue = { true },
modifier = Modifier.testTag(TEST_TAG),
colors =
ProgressIndicatorDefaults.colors(
@@ -267,7 +267,7 @@
setContentWithTheme {
SegmentedCircularProgressIndicator(
segmentCount = 6,
- completed = { false },
+ segmentValue = { false },
modifier = Modifier.testTag(TEST_TAG),
colors =
ProgressIndicatorDefaults.colors(
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwipeToRevealScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwipeToRevealScreenshotTest.kt
new file mode 100644
index 0000000..5c2711f
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwipeToRevealScreenshotTest.kt
@@ -0,0 +1,241 @@
+/*
+ * 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.wear.compose.material3
+
+import android.os.Build
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Close
+import androidx.compose.material.icons.outlined.MoreVert
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
+import androidx.wear.compose.foundation.RevealActionType
+import androidx.wear.compose.foundation.RevealValue
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(TestParameterInjector::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class SwipeToRevealScreenshotTest {
+ @get:Rule val rule = createComposeRule()
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
+
+ @get:Rule val testName = TestName()
+
+ @OptIn(ExperimentalWearFoundationApi::class)
+ @Test
+ fun swipeToReveal_showsPrimaryAction(@TestParameter screenSize: ScreenSize) {
+ verifyScreenshotForSize(screenSize) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SwipeToReveal(
+ modifier = Modifier.testTag(TEST_TAG),
+ revealState = rememberRevealState(initialValue = RevealValue.Revealing),
+ actions = {
+ primaryAction(
+ {},
+ { Icon(Icons.Outlined.Close, contentDescription = "Clear") },
+ "Clear"
+ )
+ }
+ ) {
+ Button({}, Modifier.fillMaxWidth()) {
+ Text("This text should be partially visible.")
+ }
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalWearFoundationApi::class)
+ @Test
+ fun swipeToReveal_showsPrimaryAndSecondaryActions(@TestParameter screenSize: ScreenSize) {
+ verifyScreenshotForSize(screenSize) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SwipeToReveal(
+ modifier = Modifier.testTag(TEST_TAG),
+ revealState =
+ rememberRevealState(
+ initialValue = RevealValue.Revealing,
+ anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth
+ ),
+ actions = {
+ primaryAction(
+ {},
+ { Icon(Icons.Outlined.Close, contentDescription = "Clear") },
+ "Clear"
+ )
+ secondaryAction(
+ {},
+ { Icon(Icons.Outlined.MoreVert, contentDescription = "More") },
+ "More"
+ )
+ }
+ ) {
+ Button({}, Modifier.fillMaxWidth()) {
+ Text("This text should be partially visible.")
+ }
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalWearFoundationApi::class)
+ @Test
+ fun swipeToReveal_showsUndoPrimaryAction(@TestParameter screenSize: ScreenSize) {
+ verifyScreenshotForSize(screenSize) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SwipeToReveal(
+ modifier = Modifier.testTag(TEST_TAG),
+ revealState = rememberRevealState(initialValue = RevealValue.Revealed),
+ actions = {
+ primaryAction({}, /* Empty for testing */ {}, /* Empty for testing */ "")
+ undoPrimaryAction({}, "Undo Primary")
+ }
+ ) {
+ Button({}) { Text(/* Empty for testing */ "") }
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalWearFoundationApi::class)
+ @Test
+ fun swipeToReveal_showsUndoSecondaryAction(@TestParameter screenSize: ScreenSize) {
+ verifyScreenshotForSize(screenSize) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SwipeToReveal(
+ modifier = Modifier.testTag(TEST_TAG),
+ revealState =
+ rememberRevealState(initialValue = RevealValue.Revealed).apply {
+ lastActionType = RevealActionType.SecondaryAction
+ },
+ actions = {
+ primaryAction({}, /* Empty for testing */ {}, /* Empty for testing */ "")
+ undoPrimaryAction({}, /* Empty for testing */ "")
+ secondaryAction({}, /* Empty for testing */ {}, /* Empty for testing */ "")
+ undoSecondaryAction({}, "Undo Secondary")
+ }
+ ) {
+ Button({}) { Text(/* Empty for testing */ "") }
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalWearFoundationApi::class)
+ @Test
+ fun swipeToReveal_showsContent(@TestParameter screenSize: ScreenSize) {
+ verifyScreenshotForSize(screenSize) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SwipeToReveal(
+ modifier = Modifier.testTag(TEST_TAG),
+ actions = {
+ primaryAction({}, /* Empty for testing */ {}, /* Empty for testing */ "")
+ }
+ ) {
+ Button({}, Modifier.fillMaxWidth()) {
+ Text("This content should be fully visible.")
+ }
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalWearFoundationApi::class)
+ @Test
+ fun swipeToRevealCard_showsLargePrimaryAction(@TestParameter screenSize: ScreenSize) {
+ verifyScreenshotForSize(screenSize) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SwipeToReveal(
+ modifier = Modifier.testTag(TEST_TAG),
+ revealState = rememberRevealState(initialValue = RevealValue.Revealing),
+ actionButtonHeight = SwipeToRevealDefaults.LargeActionButtonHeight,
+ actions = {
+ primaryAction(
+ {},
+ { Icon(Icons.Outlined.Close, contentDescription = "Clear") },
+ "Clear"
+ )
+ }
+ ) {
+ Card({}, Modifier.fillMaxWidth()) {
+ Text("This content should be partially visible.")
+ }
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalWearFoundationApi::class)
+ @Test
+ fun swipeToRevealCard_showsLargePrimaryAndSecondaryActions(
+ @TestParameter screenSize: ScreenSize
+ ) {
+ verifyScreenshotForSize(screenSize) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SwipeToReveal(
+ modifier = Modifier.testTag(TEST_TAG),
+ revealState =
+ rememberRevealState(
+ initialValue = RevealValue.Revealing,
+ anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth
+ ),
+ actionButtonHeight = SwipeToRevealDefaults.LargeActionButtonHeight,
+ actions = {
+ primaryAction(
+ {},
+ { Icon(Icons.Outlined.Close, contentDescription = "Clear") },
+ "Clear"
+ )
+ secondaryAction(
+ {},
+ { Icon(Icons.Outlined.MoreVert, contentDescription = "More") },
+ "More"
+ )
+ }
+ ) {
+ Card({}, Modifier.fillMaxWidth()) {
+ Text("This content should be partially visible.")
+ }
+ }
+ }
+ }
+ }
+
+ private fun verifyScreenshotForSize(screenSize: ScreenSize, content: @Composable () -> Unit) {
+ rule.verifyScreenshot(
+ screenshotRule = screenshotRule,
+ methodName = testName.goldenIdentifier()
+ ) {
+ ScreenConfiguration(screenSize.size) { content() }
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PickerGroup.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PickerGroup.kt
index 855847c..dac17a7 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PickerGroup.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PickerGroup.kt
@@ -261,7 +261,7 @@
if (propagateMinConstraints) {
parentConstraints
} else {
- parentConstraints.copy(minWidth = 0, minHeight = 0)
+ parentConstraints.copyMaxDimensions()
}
val placeables = measurables.fastMap { it.measure(constraints) }
val centeringOffset = computeCenteringOffset(placeables)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt
index c212948..f919040 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt
@@ -99,15 +99,16 @@
*
* Example of [SegmentedCircularProgressIndicator] where the segments are turned on/off:
*
- * @sample androidx.wear.compose.material3.samples.SegmentedProgressIndicatorOnOffSample
+ * @sample androidx.wear.compose.material3.samples.SegmentedProgressIndicatorBinarySample
*
* Example of smaller size [SegmentedCircularProgressIndicator]:
*
* @sample androidx.wear.compose.material3.samples.SmallSegmentedProgressIndicatorSample
* @param segmentCount Number of equal segments that the progress indicator should be divided into.
* Has to be a number equal or greater to 1.
- * @param completed A function that for each segment between 1..[segmentCount] returns true if this
- * segment has been completed, and false if this segment has not been completed.
+ * @param segmentValue A function that for each segment between 1..[segmentCount] returns true if
+ * this segment should be displayed with the indicator color to show progress, and false if the
+ * segment should be displayed with the track color.
* @param modifier Modifier to be applied to the SegmentedCircularProgressIndicator.
* @param startAngle The starting position of the progress arc, measured clockwise in degrees (0
* to 360) from the 3 o'clock position. For example, 0 and 360 represent 3 o'clock, 90 and 180
@@ -127,7 +128,7 @@
@Composable
fun SegmentedCircularProgressIndicator(
@IntRange(from = 1) segmentCount: Int,
- completed: (segmentIndex: Int) -> Boolean,
+ segmentValue: (segmentIndex: Int) -> Boolean,
modifier: Modifier = Modifier,
startAngle: Float = CircularProgressIndicatorDefaults.StartAngle,
endAngle: Float = startAngle,
@@ -137,7 +138,7 @@
enabled: Boolean = true,
) =
SegmentedCircularProgressIndicatorImpl(
- segmentParams = SegmentParams.Completed(completed),
+ segmentParams = SegmentParams.Binary(segmentValue),
modifier = modifier,
segmentCount = segmentCount,
startAngle = startAngle,
@@ -183,9 +184,9 @@
(if (segmentCount > 1) gapSweep / 2 else 0f)
when (segmentParams) {
- is SegmentParams.Completed -> {
+ is SegmentParams.Binary -> {
val color =
- if (segmentParams.completed(segment))
+ if (segmentParams.segmentValue(segment))
colors.indicatorBrush(enabled)
else colors.trackBrush(enabled)
@@ -240,7 +241,7 @@
}
private sealed interface SegmentParams {
- data class Completed(val completed: (segmentIndex: Int) -> Boolean) : SegmentParams
+ data class Binary(val segmentValue: (segmentIndex: Int) -> Boolean) : SegmentParams
data class Progress(val progress: () -> Float, val allowOverflow: Boolean) : SegmentParams
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToReveal.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToReveal.kt
new file mode 100644
index 0000000..5027491
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToReveal.kt
@@ -0,0 +1,535 @@
+/*
+ * 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.wear.compose.material3
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.expandHorizontally
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.animation.shrinkHorizontally
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.takeOrElse
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
+import androidx.wear.compose.foundation.RevealActionType
+import androidx.wear.compose.foundation.RevealState
+import androidx.wear.compose.foundation.RevealValue
+import androidx.wear.compose.foundation.SwipeToReveal
+import androidx.wear.compose.foundation.createAnchors
+import androidx.wear.compose.material3.ButtonDefaults.buttonColors
+import androidx.wear.compose.material3.tokens.SwipeToRevealTokens
+import androidx.wear.compose.materialcore.screenWidthDp
+import kotlin.math.abs
+import kotlinx.coroutines.launch
+
+/**
+ * [SwipeToReveal] Material composable. This adds the option to configure up to two additional
+ * actions on a Composable: a mandatory [SwipeToRevealScope.primaryAction] and an optional
+ * [SwipeToRevealScope.secondaryAction]. These actions are initially hidden and revealed only when
+ * the [content] is swiped. These additional actions can be triggered by clicking on them after they
+ * are revealed. [SwipeToRevealScope.primaryAction] will be triggered on full swipe of the
+ * [content].
+ *
+ * For actions like "Delete", consider adding [SwipeToRevealScope.undoPrimaryAction] (displayed when
+ * the [SwipeToRevealScope.primaryAction] is activated). Adding undo composables allow users to undo
+ * the action that they just performed.
+ *
+ * [SwipeToReveal] composable adds the [CustomAccessibilityAction]s using the labels from primary
+ * and secondary actions.
+ *
+ * Example of [SwipeToReveal] with primary and secondary actions
+ *
+ * @sample androidx.wear.compose.material3.samples.SwipeToRevealSample
+ *
+ * Example of [SwipeToReveal] with a Card composable, it reveals a taller button.
+ *
+ * @sample androidx.wear.compose.material3.samples.SwipeToRevealSingleActionCardSample
+ *
+ * Example of [SwipeToReveal] that doesn't reveal the actions, instead it only executes them when
+ * fully swiped or bounces back to its initial state.
+ *
+ * @sample androidx.wear.compose.material3.samples.SwipeToRevealNonAnchoredSample
+ * @param actions Actions of the [SwipeToReveal] composable, such as
+ * [SwipeToRevealScope.primaryAction]. [actions] should always include exactly one
+ * [SwipeToRevealScope.primaryAction]. [SwipeToRevealScope.secondaryAction],
+ * [SwipeToRevealScope.undoPrimaryAction] and [SwipeToRevealScope.undoSecondaryAction] are
+ * optional.
+ * @param modifier [Modifier] to be applied on the composable
+ * @param revealState [RevealState] of the [SwipeToReveal]
+ * @param actionButtonHeight Desired height of the revealed action buttons. In case the content is a
+ * Button composable, it's suggested to use [SwipeToRevealDefaults.SmallActionButtonHeight], and
+ * for a Card composable, it's suggested to use [SwipeToRevealDefaults.LargeActionButtonHeight].
+ * @param content The content that will be initially displayed over the other actions provided.
+ * @see [androidx.wear.compose.foundation.SwipeToReveal]
+ */
+@OptIn(ExperimentalWearFoundationApi::class)
+@Composable
+fun SwipeToReveal(
+ actions: SwipeToRevealScope.() -> Unit,
+ modifier: Modifier = Modifier,
+ revealState: RevealState = rememberRevealState(),
+ actionButtonHeight: Dp = SwipeToRevealDefaults.SmallActionButtonHeight,
+ content: @Composable () -> Unit,
+) {
+ val children = SwipeToRevealScope()
+ with(children, actions)
+ val primaryAction = children.primaryAction
+ require(primaryAction != null) {
+ "PrimaryAction should be provided in actions by calling the PrimaryAction method"
+ }
+
+ SwipeToReveal(
+ modifier =
+ modifier.fillMaxWidth().semantics {
+ customActions = buildList {
+ add(
+ CustomAccessibilityAction(primaryAction.label) {
+ primaryAction.onClick()
+ true
+ }
+ )
+ children.secondaryAction?.let {
+ add(
+ CustomAccessibilityAction(it.label) {
+ it.onClick()
+ true
+ }
+ )
+ }
+ }
+ },
+ primaryAction = {
+ ActionButton(
+ revealState,
+ primaryAction,
+ RevealActionType.PrimaryAction,
+ actionButtonHeight,
+ children.undoPrimaryAction != null,
+ )
+ },
+ secondaryAction =
+ children.secondaryAction?.let {
+ {
+ ActionButton(
+ revealState,
+ it,
+ RevealActionType.SecondaryAction,
+ actionButtonHeight,
+ children.undoSecondaryAction != null,
+ )
+ }
+ },
+ undoAction =
+ when (revealState.lastActionType) {
+ RevealActionType.SecondaryAction ->
+ children.undoSecondaryAction?.let {
+ {
+ ActionButton(
+ revealState,
+ it,
+ RevealActionType.UndoAction,
+ actionButtonHeight,
+ )
+ }
+ }
+ else ->
+ children.undoPrimaryAction?.let {
+ {
+ ActionButton(
+ revealState,
+ it,
+ RevealActionType.UndoAction,
+ actionButtonHeight,
+ )
+ }
+ }
+ },
+ onFullSwipe = {
+ // Full swipe triggers the main action, but does not set the click type.
+ // Explicitly set the click type as main action when full swipe occurs.
+ revealState.lastActionType = RevealActionType.PrimaryAction
+ primaryAction.onClick()
+ },
+ state = revealState,
+ content = content,
+ )
+}
+
+/**
+ * Scope for the actions of a [SwipeToReveal] composable. Used to define the primary, secondary,
+ * undo primary and undo secondary actions.
+ */
+class SwipeToRevealScope {
+ /**
+ * Adds the primary action to a [SwipeToReveal]. This is required and exactly one primary action
+ * should be specified. In case there are multiple, only the latest one will be displayed.
+ *
+ * @param onClick Callback to be executed when the action is performed via a full swipe, or a
+ * button click.
+ * @param icon Icon composable to be displayed for this action.
+ * @param label Label for this action. Used to create a [CustomAccessibilityAction] for the
+ * [SwipeToReveal] component, and to display what the action is when the user fully swipes to
+ * execute the primary action.
+ * @param containerColor Container color for this action.
+ * @param contentColor Content color for this action.
+ */
+ fun primaryAction(
+ onClick: () -> Unit,
+ icon: @Composable () -> Unit,
+ label: String,
+ containerColor: Color = Color.Unspecified,
+ contentColor: Color = Color.Unspecified
+ ) {
+ primaryAction = SwipeToRevealAction(onClick, icon, label, containerColor, contentColor)
+ }
+
+ /**
+ * Adds the secondary action to a [SwipeToReveal]. This is optional and at most one secondary
+ * action should be specified. In case there are multiple, only the latest one will be
+ * displayed.
+ *
+ * @param onClick Callback to be executed when the action is performed via a button click.
+ * @param icon Icon composable to be displayed for this action.
+ * @param label Label for this action. Used to create a [CustomAccessibilityAction] for the
+ * [SwipeToReveal] component.
+ * @param containerColor Container color for this action.
+ * @param contentColor Content color for this action.
+ */
+ fun secondaryAction(
+ onClick: () -> Unit,
+ icon: @Composable () -> Unit,
+ label: String,
+ containerColor: Color = Color.Unspecified,
+ contentColor: Color = Color.Unspecified
+ ) {
+ secondaryAction = SwipeToRevealAction(onClick, icon, label, containerColor, contentColor)
+ }
+
+ /**
+ * Adds the undo action for the primary action to a [SwipeToReveal]. Displayed after the user
+ * performs the primary action. This is optional and at most one undo primary action should be
+ * specified. In case there are multiple, only the latest one will be displayed.
+ *
+ * @param onClick Callback to be executed when the action is performed via a button click.
+ * @param label Label for this action. Used to display what the undo action is after the user
+ * executes the primary action.
+ * @param icon Optional Icon composable to be displayed for this action.
+ * @param containerColor Container color for this action.
+ * @param contentColor Content color for this action.
+ */
+ fun undoPrimaryAction(
+ onClick: () -> Unit,
+ label: String,
+ icon: @Composable (() -> Unit)? = null,
+ containerColor: Color = Color.Unspecified,
+ contentColor: Color = Color.Unspecified
+ ) {
+ undoPrimaryAction = SwipeToRevealAction(onClick, icon, label, containerColor, contentColor)
+ }
+
+ /**
+ * Adds the undo action for the secondary action to a [SwipeToReveal]. Displayed after the user
+ * performs the secondary action.This is optional and at most one undo secondary action should
+ * be specified. In case there are multiple, only the latest one will be displayed.
+ *
+ * @param onClick Callback to be executed when the action is performed via a button click.
+ * @param label Label for this action. Used to display what the undo action is after the user
+ * executes the secondary action.
+ * @param icon Optional Icon composable to be displayed for this action.
+ * @param containerColor Container color for this action.
+ * @param contentColor Content color for this action.
+ */
+ fun undoSecondaryAction(
+ onClick: () -> Unit,
+ label: String,
+ icon: @Composable (() -> Unit)? = null,
+ containerColor: Color = Color.Unspecified,
+ contentColor: Color = Color.Unspecified
+ ) {
+ undoSecondaryAction =
+ SwipeToRevealAction(onClick, icon, label, containerColor, contentColor)
+ }
+
+ internal var primaryAction: SwipeToRevealAction? = null
+ internal var undoPrimaryAction: SwipeToRevealAction? = null
+ internal var secondaryAction: SwipeToRevealAction? = null
+ internal var undoSecondaryAction: SwipeToRevealAction? = null
+}
+
+/**
+ * Creates a reveal state with Material3 specs.
+ *
+ * @param initialValue The initial value of the [RevealValue] for the [SwipeToReveal] composable.
+ * @param anchorWidth Fraction of the screen revealed items should be displayed in. Ignored if
+ * [useAnchoredActions] is set to false, as the items won't be anchored to the screen. For a
+ * single action SwipeToReveal component, this should be
+ * [SwipeToRevealDefaults.SingleActionAnchorWidth], and for a double action SwipeToReveal,
+ * [SwipeToRevealDefaults.DoubleActionAnchorWidth] to be able to display both action buttons.
+ * @param useAnchoredActions Whether the actions should stay revealed, or bounce back to hidden when
+ * the user stops swiping. This is relevant for SwipeToReveal components with a single action. If
+ * the developer wants a swipe to clear behaviour, this should be set to false.
+ */
+@OptIn(ExperimentalWearFoundationApi::class)
+@Composable
+fun rememberRevealState(
+ initialValue: RevealValue = RevealValue.Covered,
+ anchorWidth: Dp = SwipeToRevealDefaults.SingleActionAnchorWidth,
+ useAnchoredActions: Boolean = true,
+): RevealState {
+ val anchorFraction = anchorWidth.value / screenWidthDp()
+ return androidx.wear.compose.foundation.rememberRevealState(
+ initialValue = initialValue,
+ animationSpec = spring(1f, Spring.StiffnessMedium),
+ anchors =
+ createAnchors(
+ revealingAnchor = if (useAnchoredActions) anchorFraction else 0f,
+ )
+ )
+}
+
+object SwipeToRevealDefaults {
+
+ /** Width that's required to display both actions in a [SwipeToReveal] composable. */
+ val DoubleActionAnchorWidth = 130.dp
+
+ /** Width that's required to display a single action in a [SwipeToReveal] composable. */
+ val SingleActionAnchorWidth = 64.dp
+
+ /** Standard height for a small revealed action, such as when the swiped item is a Button. */
+ val SmallActionButtonHeight = 52.dp
+
+ /** Standard height for a large revealed action, such as when the swiped item is a Card. */
+ val LargeActionButtonHeight = 84.dp
+
+ internal val MinimumIconSize = 20.dp
+
+ internal val IconSize = 26.dp
+
+ internal val IconAndTextPadding = 6.dp
+
+ internal val ActionButtonContentPadding = 4.dp
+
+ internal val FullScreenPaddingFraction = 0.0625f
+}
+
+@OptIn(ExperimentalWearFoundationApi::class)
+@Composable
+internal fun ActionButton(
+ revealState: RevealState,
+ action: SwipeToRevealAction,
+ revealActionType: RevealActionType,
+ buttonHeight: Dp,
+ hasUndo: Boolean = false,
+) {
+ val containerColor =
+ action.containerColor.takeOrElse {
+ when (revealActionType) {
+ RevealActionType.PrimaryAction ->
+ MaterialTheme.colorScheme.fromToken(
+ SwipeToRevealTokens.PrimaryActionContainerColor
+ )
+ RevealActionType.SecondaryAction ->
+ MaterialTheme.colorScheme.fromToken(
+ SwipeToRevealTokens.SecondaryActionContainerColor
+ )
+ RevealActionType.UndoAction ->
+ MaterialTheme.colorScheme.fromToken(
+ SwipeToRevealTokens.UndoActionContainerColor
+ )
+ else -> Color.Unspecified
+ }
+ }
+ val contentColor =
+ action.contentColor.takeOrElse {
+ when (revealActionType) {
+ RevealActionType.PrimaryAction ->
+ MaterialTheme.colorScheme.fromToken(
+ SwipeToRevealTokens.PrimaryActionContentColor
+ )
+ RevealActionType.SecondaryAction ->
+ MaterialTheme.colorScheme.fromToken(
+ SwipeToRevealTokens.SecondaryActionContentColor
+ )
+ RevealActionType.UndoAction ->
+ MaterialTheme.colorScheme.fromToken(SwipeToRevealTokens.UndoActionContentColor)
+ else -> Color.Unspecified
+ }
+ }
+ val fullScreenPaddingDp = (screenWidthDp() * SwipeToRevealDefaults.FullScreenPaddingFraction).dp
+ val startPadding =
+ when (revealActionType) {
+ RevealActionType.UndoAction -> fullScreenPaddingDp
+ else -> 0.dp
+ }
+ val endPadding =
+ when (revealActionType) {
+ RevealActionType.UndoAction -> fullScreenPaddingDp
+ else -> 0.dp
+ }
+ val coroutineScope = rememberCoroutineScope()
+ Button(
+ modifier =
+ Modifier.height(buttonHeight)
+ .padding(startPadding, 0.dp, endPadding, 0.dp)
+ .fillMaxWidth(),
+ onClick = {
+ coroutineScope.launch {
+ try {
+ if (revealActionType == RevealActionType.UndoAction) {
+ revealState.animateTo(RevealValue.Covered)
+ } else {
+ if (hasUndo || revealActionType == RevealActionType.PrimaryAction) {
+ revealState.lastActionType = revealActionType
+ revealState.animateTo(RevealValue.Revealed)
+ }
+ }
+ } finally {
+ // Execute onClick even if the animation gets interrupted
+ action.onClick()
+ }
+ }
+ },
+ colors = buttonColors(containerColor = containerColor, contentColor = contentColor),
+ contentPadding = PaddingValues(SwipeToRevealDefaults.ActionButtonContentPadding),
+ shape = CircleShape
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth().fillMaxHeight(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val density = LocalDensity.current
+ val primaryActionTextRevealed = remember { mutableStateOf(false) }
+ action.icon?.let { ActionIconWrapper(it) }
+ when (revealActionType) {
+ RevealActionType.PrimaryAction ->
+ AnimatedVisibility(
+ visible = primaryActionTextRevealed.value,
+ enter = fadeIn() + expandHorizontally() + scaleIn(),
+ exit = fadeOut() + shrinkHorizontally() + scaleOut(),
+ ) {
+ ActionText(action, contentColor)
+ }
+ RevealActionType.UndoAction -> ActionText(action, contentColor)
+ }
+ if (revealActionType == RevealActionType.PrimaryAction) {
+ LaunchedEffect(revealState.offset) {
+ val minimumOffsetToRevealPx =
+ with(density) {
+ SwipeToRevealDefaults.DoubleActionAnchorWidth.toPx().toInt()
+ }
+ primaryActionTextRevealed.value =
+ abs(revealState.offset) > minimumOffsetToRevealPx &&
+ revealState.targetValue == RevealValue.Revealed
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ActionText(action: SwipeToRevealAction, contentColor: Color) {
+ Text(
+ modifier =
+ Modifier.padding(
+ start = action.icon?.let { SwipeToRevealDefaults.IconAndTextPadding } ?: 0.dp
+ ),
+ text = action.label,
+ color = contentColor,
+ maxLines = 1
+ )
+}
+
+@Composable
+private fun ActionIconWrapper(content: @Composable () -> Unit) {
+ val iconAlpha = remember { mutableFloatStateOf(0f) }
+ Box(
+ modifier =
+ Modifier.onGloballyPositioned { coordinates ->
+ val currentWidthDp = coordinates.size.width.dp.value
+ iconAlpha.floatValue =
+ ((currentWidthDp - SwipeToRevealDefaults.MinimumIconSize.value) /
+ (SwipeToRevealDefaults.IconSize.value -
+ SwipeToRevealDefaults.MinimumIconSize.value))
+ .coerceIn(0.0f, 1.0f)
+ }
+ .size(SwipeToRevealDefaults.IconSize, Dp.Unspecified)
+ .graphicsLayer { alpha = iconAlpha.floatValue }
+ ) {
+ content()
+ }
+}
+
+/** Data class to define an action to be displayed in a [SwipeToReveal] composable. */
+internal data class SwipeToRevealAction(
+ /** Callback to be executed when the action is performed via a full swipe, or a button click. */
+ val onClick: () -> Unit,
+
+ /**
+ * Icon composable to be displayed for this action. This accepts a scale parameter that should
+ * be used to increase icon icon when an action is fully revealed.
+ */
+ val icon: @Composable (() -> Unit)?,
+
+ /**
+ * Label for this action. Used to create a [CustomAccessibilityAction] for the [SwipeToReveal]
+ * component, display what the action is when the user fully swipes to execute the primary
+ * action, or when the undo action is shown.
+ */
+ val label: String,
+
+ /**
+ * Color of the container, used for the background of the action button. This can be
+ * [Color.Unspecified], and in case it is, needs to be replaced with a default.
+ */
+ val containerColor: Color,
+
+ /**
+ * Color of the content, used for the icon and text. This can be [Color.Unspecified], and in
+ * case it is, needs to be replaced with a default.
+ */
+ val contentColor: Color,
+)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/SwipeToRevealTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/SwipeToRevealTokens.kt
new file mode 100644
index 0000000..a82d210
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/SwipeToRevealTokens.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.wear.compose.material3.tokens
+
+internal object SwipeToRevealTokens {
+ val PrimaryActionContainerColor = ColorSchemeKeyTokens.Error
+ val PrimaryActionContentColor = ColorSchemeKeyTokens.OnError
+ val SecondaryActionContainerColor = ColorSchemeKeyTokens.SurfaceContainer
+ val SecondaryActionContentColor = ColorSchemeKeyTokens.OnSurface
+ val UndoActionContainerColor = ColorSchemeKeyTokens.SurfaceContainer
+ val UndoActionContentColor = ColorSchemeKeyTokens.OnSurface
+}
diff --git a/wear/compose/compose-material3/src/main/res/values-af/strings.xml b/wear/compose/compose-material3/src/main/res/values-af/strings.xml
index 8ee35d5..292ca6d 100644
--- a/wear/compose/compose-material3/src/main/res/values-af/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-af/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Jaar"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bevestig"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Volgende"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Het misluk"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Sukses"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Maak op foon oop"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-am/strings.xml b/wear/compose/compose-material3/src/main/res/values-am/strings.xml
index d992a69..1f165fd 100644
--- a/wear/compose/compose-material3/src/main/res/values-am/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-am/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ዓመት"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"አረጋግጥ"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ቀጣይ"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"አልተሳካም"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ተሳክቷል"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ስልክ ላይ ክፈት"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ar/strings.xml b/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
index 95f1916..b39dc3c6 100644
--- a/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
@@ -50,6 +50,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"السنة"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تأكيد"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"التالي"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"تعذر الإجراء"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"نجحَ الإجراء"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"فتح على الهاتف"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-as/strings.xml b/wear/compose/compose-material3/src/main/res/values-as/strings.xml
index 81be021..d2f2b88 100644
--- a/wear/compose/compose-material3/src/main/res/values-as/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-as/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"বছৰ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"নিশ্চিত কৰক"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"পৰৱৰ্তী"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"বিফল হৈছে"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"সফল"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ফ’নত খোলক"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-az/strings.xml b/wear/compose/compose-material3/src/main/res/values-az/strings.xml
index 67de877..a9b81c0 100644
--- a/wear/compose/compose-material3/src/main/res/values-az/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-az/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"İl"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Təsdiq edin"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Növbəti"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Alınmadı"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Alındı"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Telefonda aç"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml b/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
index 5d034c1..88e265dc 100644
--- a/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdi"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalje"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nije uspelo"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Uspelo"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Na telefonu"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-be/strings.xml b/wear/compose/compose-material3/src/main/res/values-be/strings.xml
index 60671a0..dd3df95 100644
--- a/wear/compose/compose-material3/src/main/res/values-be/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-be/strings.xml
@@ -44,6 +44,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Год"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Пацвердзіць"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далей"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Памылка"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Выканана"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"На тэлефоне"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-bg/strings.xml b/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
index 6461c85..df9c2d1 100644
--- a/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потвърждаване"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Напред"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Неуспешно"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Успешно"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Отв. на тел."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-bn/strings.xml b/wear/compose/compose-material3/src/main/res/values-bn/strings.xml
index ee3b71a4..148c088 100644
--- a/wear/compose/compose-material3/src/main/res/values-bn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-bn/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"বছর"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"কনফার্ম করুন"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"পরবর্তী"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"সফল হয়নি"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"সফল হয়েছে"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ফোনে খুলুন"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-bs/strings.xml b/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
index eb4a11b..82bc2f2 100644
--- a/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrđivanje"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Naprijed"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Neuspješno"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Uspješno"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otvor. na tel."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ca/strings.xml b/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
index 5791ded..1a58fc7fa 100644
--- a/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Any"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirma"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Següent"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ha fallat"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Correcte"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Obre al telèfon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-cs/strings.xml b/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
index bfe3053..63526fc 100644
--- a/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
@@ -44,6 +44,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdit"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Další"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nezdařilo se"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Hotovo"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otevřít v telefonu"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-da/strings.xml b/wear/compose/compose-material3/src/main/res/values-da/strings.xml
index 607983d0..fc69382 100644
--- a/wear/compose/compose-material3/src/main/res/values-da/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-da/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekræft"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Næste"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Mislykket"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Gennemført"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Åbn på telefon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-de/strings.xml b/wear/compose/compose-material3/src/main/res/values-de/strings.xml
index 1cb073d..8ba37fe 100644
--- a/wear/compose/compose-material3/src/main/res/values-de/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-de/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Jahr"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bestätigen"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Weiter"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Fehlgeschlagen"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Abgeschlossen"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Auf Smartphone öffnen"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-el/strings.xml b/wear/compose/compose-material3/src/main/res/values-el/strings.xml
index fd2fba0..625ca9a 100644
--- a/wear/compose/compose-material3/src/main/res/values-el/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-el/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Έτος"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Επιβεβαίωση"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Επόμενο"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Αποτυχία"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Επιτυχία"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Στο τηλέφωνο"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
index 8c50eea..36aabdf 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml
index c3f406c..84db4c9 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml
@@ -38,6 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Decrease"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Increase"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
index 8c50eea..36aabdf 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
index 8c50eea..36aabdf 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml
index b6b8b0a..879aa79 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml
@@ -38,6 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Decrease"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Increase"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml b/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml
index 1eab19a..43c6298 100644
--- a/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Año"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Siguiente"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Error"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Listo"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abrir en el teléfono"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-es/strings.xml b/wear/compose/compose-material3/src/main/res/values-es/strings.xml
index 5f76983..ec62941 100644
--- a/wear/compose/compose-material3/src/main/res/values-es/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-es/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Año"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Siguiente"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Error"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Todo correcto"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ábrelo en el teléfono"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-et/strings.xml b/wear/compose/compose-material3/src/main/res/values-et/strings.xml
index 03c079a..1ce36e9 100644
--- a/wear/compose/compose-material3/src/main/res/values-et/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-et/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Aasta"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Kinnita"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Järgmine"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ebaõnnestus"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Õnnestus"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ava telefonis"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-eu/strings.xml b/wear/compose/compose-material3/src/main/res/values-eu/strings.xml
index 8c273aa..54569ad 100644
--- a/wear/compose/compose-material3/src/main/res/values-eu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-eu/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Urtea"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Berretsi"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Hurrengoa"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Huts egin du"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Eginda"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ireki telefonoan"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-fa/strings.xml b/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
index 064af35..4ed7b8a 100644
--- a/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"سال"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تأیید کردن"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"بعدی"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"انجام نشد"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"انجام شد"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"باز کردن در تلفن"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-fi/strings.xml b/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
index 2a33291..04460eb 100644
--- a/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Vuosi"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Vahvista"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seuraava"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Epäonnistui"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Onnistui"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Puhelimella"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml b/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
index 4d883e4..b3e47a1 100644
--- a/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Année"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmer"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Suivant"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Échec"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Réussite"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ouv. ds tél."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-fr/strings.xml b/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
index 9be8df2..df054a0 100644
--- a/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Année"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmer"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Suivant"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Échec"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Opération réussie"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ouvrir sur le téléphone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-gl/strings.xml b/wear/compose/compose-material3/src/main/res/values-gl/strings.xml
index c956d9e..b25a8a0 100644
--- a/wear/compose/compose-material3/src/main/res/values-gl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-gl/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seguinte"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Erro"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Todo correcto"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abrir no tel."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-gu/strings.xml b/wear/compose/compose-material3/src/main/res/values-gu/strings.xml
index ac3c8cd..05e5bed 100644
--- a/wear/compose/compose-material3/src/main/res/values-gu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-gu/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"વર્ષ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"કન્ફર્મ કરો"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"આગળ"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"નિષ્ફળ થઈ"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"સફળ થઈ"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ફોન પર ખોલો"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-hi/strings.xml b/wear/compose/compose-material3/src/main/res/values-hi/strings.xml
index 34a8cc3..629307f 100644
--- a/wear/compose/compose-material3/src/main/res/values-hi/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hi/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"साल"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"पुष्टि करें"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"अगला"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"काम नहीं हुआ"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"काम हो गया"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"फ़ोन पर खोलें"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-hr/strings.xml b/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
index e2a13ebf..f36d824 100644
--- a/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdi"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalje"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nije uspjelo"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Uspjeh"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Na telefonu"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-hu/strings.xml b/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
index 625082c..fd95148 100644
--- a/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Év"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Megerősítés"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Következő"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Sikertelen"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Sikerült"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Nyissa meg mobilon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-hy/strings.xml b/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
index b3fb75b..7b98efb 100644
--- a/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Տարի"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Հաստատել"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Հաջորդը"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ձախողվել է"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Պատրաստ է"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Բացեք հեռախոսում"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-in/strings.xml b/wear/compose/compose-material3/src/main/res/values-in/strings.xml
index c10be64..b41351a 100644
--- a/wear/compose/compose-material3/src/main/res/values-in/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-in/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Tahun"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Konfirmasi"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Berikutnya"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Gagal"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Berhasil"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Buka di ponsel"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-is/strings.xml b/wear/compose/compose-material3/src/main/res/values-is/strings.xml
index 022daa2..5b50231 100644
--- a/wear/compose/compose-material3/src/main/res/values-is/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-is/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ár"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Staðfesta"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Áfram"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Mistókst"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Tókst"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Opna í símanum"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-it/strings.xml b/wear/compose/compose-material3/src/main/res/values-it/strings.xml
index f592436..3a0f0b5 100644
--- a/wear/compose/compose-material3/src/main/res/values-it/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-it/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Anno"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Conferma"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avanti"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Non riuscita"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Riuscita"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Su smartph."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-iw/strings.xml b/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
index 8e05ba1..d0a9318 100644
--- a/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"שנה"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"אישור"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"הבא"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"הפעולה נכשלה"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"הפעולה הצליחה"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"פתיחה בטלפון"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ja/strings.xml b/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
index 0604d20..1021184 100644
--- a/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"次へ"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失敗"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"スマホで開く"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ka/strings.xml b/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
index 2558c1d..ac02463 100644
--- a/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"წელი"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"დადასტურება"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"შემდეგი"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ვერ შესრულდა"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"შესრულდა"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ტელეფონში გახსნა"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-kk/strings.xml b/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
index 6c27666..05e8014 100644
--- a/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Жыл"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Растау"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Келесі"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Расталмады."</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Расталды."</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Телефоннан ашыңыз."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-km/strings.xml b/wear/compose/compose-material3/src/main/res/values-km/strings.xml
index c2f812d..b98f36b 100644
--- a/wear/compose/compose-material3/src/main/res/values-km/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-km/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ឆ្នាំ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"បញ្ជាក់"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"បន្ទាប់"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"មិនបានសម្រេច"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ជោគជ័យ"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"បើកលើទូរសព្ទ"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-kn/strings.xml b/wear/compose/compose-material3/src/main/res/values-kn/strings.xml
index eac8e18..8b9bb7b 100644
--- a/wear/compose/compose-material3/src/main/res/values-kn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-kn/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ವರ್ಷ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ದೃಢೀಕರಿಸಿ"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ಮುಂದಿನದು"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ವಿಫಲವಾಗಿದೆ"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ಯಶಸ್ವಿಯಾಗಿದೆ"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ಫೋನ್ನಲ್ಲಿ ತೆರೆಯಿರಿ"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ko/strings.xml b/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
index 58a3c15..1643534 100644
--- a/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"년"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"확인"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"다음"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"실패"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"성공"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"휴대전화에서 열기"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ky/strings.xml b/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
index 0cd62d9..c312a62 100644
--- a/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Жыл"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Ырастоо"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Кийинки"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ишке ашпады"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Ийгилик"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Телефондо ачуу"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-lo/strings.xml b/wear/compose/compose-material3/src/main/res/values-lo/strings.xml
index 5914732..8dd107b 100644
--- a/wear/compose/compose-material3/src/main/res/values-lo/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-lo/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ປີ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ຢືນຢັນ"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ຕໍ່ໄປ"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ບໍ່ສຳເລັດ"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ສຳເລັດ"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ເປີດໃນໂທລະສັບ"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-lt/strings.xml b/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
index 81eeef5..8397c2a 100644
--- a/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
@@ -44,6 +44,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Metai"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Patvirtinti"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Kitas"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nepavyko"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Pavyko"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Atidaryti telefone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-lv/strings.xml b/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
index eb34815..c440c8bf 100644
--- a/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Gads"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Apstiprināt"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Tālāk"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Neizdevās"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Izdevās"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Atvērt tālrunī"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-mk/strings.xml b/wear/compose/compose-material3/src/main/res/values-mk/strings.xml
index 7364597..26538a6 100644
--- a/wear/compose/compose-material3/src/main/res/values-mk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-mk/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потврди"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Следно"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Неуспешно"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Успешно"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Отвори на телефонот"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ml/strings.xml b/wear/compose/compose-material3/src/main/res/values-ml/strings.xml
index 102fbe14..913ea72 100644
--- a/wear/compose/compose-material3/src/main/res/values-ml/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ml/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"വർഷം"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"സ്ഥിരീകരിക്കുക"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"അടുത്തത്"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"പരാജയപ്പെട്ടു"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"വിജയിച്ചു"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ഫോണിൽ തുറക്കൂ"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-mn/strings.xml b/wear/compose/compose-material3/src/main/res/values-mn/strings.xml
index 4d8658f..b0a2c5d 100644
--- a/wear/compose/compose-material3/src/main/res/values-mn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-mn/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Он"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Баталгаажуулах"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Дараах"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Амжилтгүй"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Амжилттай"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Утсанд нээх"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-mr/strings.xml b/wear/compose/compose-material3/src/main/res/values-mr/strings.xml
index dbcc0e28..e308c6a 100644
--- a/wear/compose/compose-material3/src/main/res/values-mr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-mr/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"वर्ष"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"कन्फर्म करा"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"पुढील"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"अयशस्वी"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"यशस्वी झाले"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"फोनवर उघडा"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ms/strings.xml b/wear/compose/compose-material3/src/main/res/values-ms/strings.xml
index b7d0922..920ea71 100644
--- a/wear/compose/compose-material3/src/main/res/values-ms/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ms/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Tahun"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Sahkan"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seterusnya"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Gagal"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Berjaya"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Buka pada telefon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-my/strings.xml b/wear/compose/compose-material3/src/main/res/values-my/strings.xml
index 4540c64..c0393c2 100644
--- a/wear/compose/compose-material3/src/main/res/values-my/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-my/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"နှစ်"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"အတည်ပြုရန်"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ရှေ့သို့"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"မအောင်မြင်ပါ"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"အောင်မြင်သည်"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ဖုန်း၌ဖွင့်ရန်"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-nb/strings.xml b/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
index 5f8fa3b..d827ac5 100644
--- a/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekreft"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Neste"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Mislyktes"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Vellykket"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Åpne på tlf."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ne/strings.xml b/wear/compose/compose-material3/src/main/res/values-ne/strings.xml
index d216dc7..3b0ad38 100644
--- a/wear/compose/compose-material3/src/main/res/values-ne/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ne/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"साल"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"पुष्टि गर्नुहोस्"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"अर्को"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"पुष्टि गर्न सकिएन"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"पुष्टि गरियो"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"फोनमा खोल्नुहोस्"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-nl/strings.xml b/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
index c57422b..d89b6e9 100644
--- a/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Jaar"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bevestigen"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Volgende"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Mislukt"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Geslaagd"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Openen op telefoon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-or/strings.xml b/wear/compose/compose-material3/src/main/res/values-or/strings.xml
index 22d20ef..261be65 100644
--- a/wear/compose/compose-material3/src/main/res/values-or/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-or/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ବର୍ଷ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ସୁନିଶ୍ଚିତ କରନ୍ତୁ"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ପରବର୍ତ୍ତୀ"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ବିଫଳ ହୋଇଛି"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ସଫଳ"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ଫୋନରେ ଖୋଲନ୍ତୁ"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-pa/strings.xml b/wear/compose/compose-material3/src/main/res/values-pa/strings.xml
index 7666dd8..c1b3a39 100644
--- a/wear/compose/compose-material3/src/main/res/values-pa/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pa/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ਸਾਲ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ਤਸਦੀਕ ਕਰੋ"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ਅੱਗੇ"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ਅਸਫਲ ਰਿਹਾ"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ਸਫਲ ਰਿਹਾ"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ਫ਼ੋਨ \'ਤੇ ਖੋਲ੍ਹੋ"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-pl/strings.xml b/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
index d330a6f..5484b3e 100644
--- a/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
@@ -44,6 +44,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potwierdź"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalej"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Niepowodzenie"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Udało się"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otwórz na telefonie"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
index 9552f13..12c2766 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avançar"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Falha"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Pronto"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abra no smartphone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
index b200da8..c4d5152 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seguinte"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Falhou"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Concluído"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abrir no tel."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
index 9552f13..12c2766 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avançar"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Falha"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Pronto"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abra no smartphone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ro/strings.xml b/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
index 2f2593f..03c63d69 100644
--- a/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"An"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmă"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Înainte"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Eroare"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Succes"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Pe telefon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ru/strings.xml b/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
index 9d251bd..84d7feb 100644
--- a/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
@@ -44,6 +44,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Год"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Подтвердить"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далее"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ошибка"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Готово"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Открыть на телефоне"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-si/strings.xml b/wear/compose/compose-material3/src/main/res/values-si/strings.xml
index f8b70ed..4a7ee74 100644
--- a/wear/compose/compose-material3/src/main/res/values-si/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-si/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"වසර"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"තහවුරු කරන්න"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"මීළඟ"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"අසමත් විය"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"සාර්ථකයි"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"දුරකථනයෙන් විවෘත කරන්න"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-sk/strings.xml b/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
index 10b51d9..0b391a8 100644
--- a/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
@@ -44,6 +44,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdiť"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Ďalej"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Neúspešné"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Podarilo sa"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otvorte v telefóne"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-sl/strings.xml b/wear/compose/compose-material3/src/main/res/values-sl/strings.xml
index 8bbca36..ac83468 100644
--- a/wear/compose/compose-material3/src/main/res/values-sl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sl/strings.xml
@@ -44,6 +44,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Leto"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potrdi"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Naprej"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Neuspešno"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Uspešno"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Odpri v telefonu"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-sq/strings.xml b/wear/compose/compose-material3/src/main/res/values-sq/strings.xml
index 392a80a..ffb42ba 100644
--- a/wear/compose/compose-material3/src/main/res/values-sq/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sq/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Viti"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Konfirmo"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Para"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Dështoi"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Me sukses"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Hape në telefon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-sr/strings.xml b/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
index 4ecd9ec..c5bdeb3 100644
--- a/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
@@ -41,6 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потврди"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Даље"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Није успело"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Успело"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"На телефону"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-sv/strings.xml b/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
index af57d5c..8e24d46 100644
--- a/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekräfta"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Nästa"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Misslyckades"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Klart"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"På telefonen"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-sw/strings.xml b/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
index 1e84af2..c2ad8cb 100644
--- a/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Mwaka"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Thibitisha"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Endelea"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Imeshindwa"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Imemaliza"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Fungua kwenye simu"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ta/strings.xml b/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
index 2e9368a..4f93096 100644
--- a/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ஆண்டு"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"உறுதிசெய்யும்"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"அடுத்ததற்குச் செல்லும்"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"தோல்வி"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"முடிந்தது"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"மொபைலில் திற"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-te/strings.xml b/wear/compose/compose-material3/src/main/res/values-te/strings.xml
index 58ac088..fa2abc2 100644
--- a/wear/compose/compose-material3/src/main/res/values-te/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-te/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"సంవత్సరం"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"నిర్ధారించండి"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"తర్వాత"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"విఫలమైంది"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"విజయవంతమైంది"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ఫోన్లో తెరు"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-th/strings.xml b/wear/compose/compose-material3/src/main/res/values-th/strings.xml
index c8e01f1..d4825e4 100644
--- a/wear/compose/compose-material3/src/main/res/values-th/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-th/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ปี"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ยืนยัน"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ถัดไป"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ไม่สำเร็จ"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"สำเร็จ"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"เปิดในโทรศัพท์"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-tl/strings.xml b/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
index e8780c2..186c0ed 100644
--- a/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Taon"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Kumpirmahin"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Susunod"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nabigo"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Matagumpay"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Buksan"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-tr/strings.xml b/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
index bb0315d..c978fe0 100644
--- a/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Yıl"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Onayla"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Sonraki"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Başarısız"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Başarılı"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Telefonda aç"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-uk/strings.xml b/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
index 855f9b8..3fa6994 100644
--- a/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
@@ -44,6 +44,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Рік"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Підтвердити"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далі"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Помилка"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Готово"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"На телефоні"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ur/strings.xml b/wear/compose/compose-material3/src/main/res/values-ur/strings.xml
index 1f94321..3232837 100644
--- a/wear/compose/compose-material3/src/main/res/values-ur/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ur/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"سال"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تصدیق کریں"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"اگلا"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ناکام ہوا"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"کامیاب"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"فون پر کھولیں"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-uz/strings.xml b/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
index 433f419..d99a721 100644
--- a/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Yil"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Tasdiqlash"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Keyingisi"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Bajarilmadi"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Bajarildi"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Telefonda"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-vi/strings.xml b/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
index 7daf971..65f5eb1 100644
--- a/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Năm"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Xác nhận"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Tiếp theo"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Lỗi"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Thành công"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Mở trên điện thoại"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
index c4f4636..c395e20 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"确认"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一个"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失败"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"在手机上打开"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
index c7a2f65..7306379 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一步"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失敗"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"在手機開啟"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
index 52e3880..dfe78d7 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一個"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失敗"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"在手機上開啟"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-zu/strings.xml b/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
index 86ca061..259ef34 100644
--- a/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
@@ -38,6 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Unyaka"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Qinisekisa"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Okulandelayo"</string>
+ <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
+ <skip />
+ <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
+ <skip />
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Yehlulekile"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Impumelelo"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Vula efonini"</string>
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index d0a3eb0..cf0b96b 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
defaultConfig {
applicationId "androidx.wear.compose.integration.demos"
minSdk 25
- versionCode 39
- versionName "1.39"
+ versionCode 40
+ versionName "1.40"
}
buildTypes {
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt
index ffb459d..e491c61 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt
@@ -25,6 +25,7 @@
import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.Until
import androidx.testutils.createCompilationParams
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runners.Parameterized
@@ -76,6 +77,7 @@
private val SWITCH = "switch"
@Test
+ @Ignore("b/366137664")
fun profile() {
baselineRule.collect(
packageName = PACKAGE_NAME,